mailblastr 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 +7 -0
- data/LICENSE +21 -0
- data/README.md +332 -0
- data/lib/mailblastr/api_keys.rb +23 -0
- data/lib/mailblastr/audiences.rb +39 -0
- data/lib/mailblastr/automations.rb +62 -0
- data/lib/mailblastr/campaigns.rb +57 -0
- data/lib/mailblastr/client.rb +208 -0
- data/lib/mailblastr/contact_properties.rb +33 -0
- data/lib/mailblastr/contacts.rb +144 -0
- data/lib/mailblastr/domains.rb +78 -0
- data/lib/mailblastr/emails.rb +113 -0
- data/lib/mailblastr/error.rb +28 -0
- data/lib/mailblastr/events.rb +35 -0
- data/lib/mailblastr/logs.rb +24 -0
- data/lib/mailblastr/polls.rb +18 -0
- data/lib/mailblastr/segments.rb +44 -0
- data/lib/mailblastr/templates.rb +42 -0
- data/lib/mailblastr/topics.rb +38 -0
- data/lib/mailblastr/version.rb +5 -0
- data/lib/mailblastr/webhooks.rb +130 -0
- data/lib/mailblastr.rb +76 -0
- metadata +83 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mailblastr
|
|
4
|
+
# Segments are DOMAIN-FIRST: `domain` is required on create and list. Names
|
|
5
|
+
# are unique within a domain (reusable across domains); every domain also
|
|
6
|
+
# carries an auto-created "General" (all contacts) segment.
|
|
7
|
+
module Segments
|
|
8
|
+
class << self
|
|
9
|
+
# POST /segments
|
|
10
|
+
# Mailblastr::Segments.create({ domain: "yourdomain.com", name: "VIP",
|
|
11
|
+
# filter: { status: "subscribed" } })
|
|
12
|
+
def create(params)
|
|
13
|
+
Client.require_domain!(params, "Segments.create")
|
|
14
|
+
Client.request(:post, "/segments", body: params)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# GET /segments/:id
|
|
18
|
+
def get(segment_id)
|
|
19
|
+
Client.request(:get, "/segments/#{Client.path_escape(segment_id)}")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# List a domain's segments (`domain` required). GET /segments?domain=
|
|
23
|
+
def list(params)
|
|
24
|
+
domain = Client.require_domain!(params, "Segments.list")
|
|
25
|
+
Client.request(:get, "/segments", query: { domain: domain }.merge(Client.pagination(params)))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Preview the contacts a segment currently resolves to. GET /segments/:id/contacts
|
|
29
|
+
def contacts(segment_id)
|
|
30
|
+
Client.request(:get, "/segments/#{Client.path_escape(segment_id)}/contacts")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# PATCH /segments/:id
|
|
34
|
+
def update(segment_id, params)
|
|
35
|
+
Client.request(:patch, "/segments/#{Client.path_escape(segment_id)}", body: params)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# DELETE /segments/:id
|
|
39
|
+
def delete(segment_id)
|
|
40
|
+
Client.request(:delete, "/segments/#{Client.path_escape(segment_id)}")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mailblastr
|
|
4
|
+
module Templates
|
|
5
|
+
class << self
|
|
6
|
+
# POST /templates — params: { name:, alias:, subject:, html:, text:, variables: [...] }
|
|
7
|
+
def create(params)
|
|
8
|
+
Client.request(:post, "/templates", body: params)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# GET /templates/:id (an alias works anywhere an id is accepted)
|
|
12
|
+
def get(template_id)
|
|
13
|
+
Client.request(:get, "/templates/#{Client.path_escape(template_id)}")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# GET /templates
|
|
17
|
+
def list(params = {})
|
|
18
|
+
Client.request(:get, "/templates", query: Client.pagination(params))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# PATCH /templates/:id
|
|
22
|
+
def update(template_id, params)
|
|
23
|
+
Client.request(:patch, "/templates/#{Client.path_escape(template_id)}", body: params)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Duplicate a template. POST /templates/:id/duplicate — params: { name:, alias: }
|
|
27
|
+
def duplicate(template_id, params = {})
|
|
28
|
+
Client.request(:post, "/templates/#{Client.path_escape(template_id)}/duplicate", body: params)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Publish a template (make its latest draft live). POST /templates/:id/publish
|
|
32
|
+
def publish(template_id)
|
|
33
|
+
Client.request(:post, "/templates/#{Client.path_escape(template_id)}/publish")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# DELETE /templates/:id
|
|
37
|
+
def delete(template_id)
|
|
38
|
+
Client.request(:delete, "/templates/#{Client.path_escape(template_id)}")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mailblastr
|
|
4
|
+
# Topics (granular subscriptions) are DOMAIN-FIRST: `domain` is required on
|
|
5
|
+
# create and list. Topic names are reusable across domains.
|
|
6
|
+
module Topics
|
|
7
|
+
class << self
|
|
8
|
+
# POST /topics
|
|
9
|
+
# Mailblastr::Topics.create({ domain: "yourdomain.com", name: "Product updates",
|
|
10
|
+
# default_subscription: "opt_in" })
|
|
11
|
+
def create(params)
|
|
12
|
+
Client.require_domain!(params, "Topics.create")
|
|
13
|
+
Client.request(:post, "/topics", body: params)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# GET /topics/:id
|
|
17
|
+
def get(topic_id)
|
|
18
|
+
Client.request(:get, "/topics/#{Client.path_escape(topic_id)}")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# List a domain's topics (`domain` required). GET /topics?domain=
|
|
22
|
+
def list(params)
|
|
23
|
+
domain = Client.require_domain!(params, "Topics.list")
|
|
24
|
+
Client.request(:get, "/topics", query: { domain: domain }.merge(Client.pagination(params)))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# PATCH /topics/:id
|
|
28
|
+
def update(topic_id, params)
|
|
29
|
+
Client.request(:patch, "/topics/#{Client.path_escape(topic_id)}", body: params)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# DELETE /topics/:id
|
|
33
|
+
def delete(topic_id)
|
|
34
|
+
Client.request(:delete, "/topics/#{Client.path_escape(topic_id)}")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
module Mailblastr
|
|
6
|
+
module Webhooks
|
|
7
|
+
class << self
|
|
8
|
+
# Create a webhook. The plaintext signing secret is returned ONCE, only here.
|
|
9
|
+
# POST /webhooks — params: { endpoint:, events: [...], secret: }
|
|
10
|
+
def create(params)
|
|
11
|
+
Client.request(:post, "/webhooks", body: params)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# GET /webhooks/:id
|
|
15
|
+
def get(webhook_id)
|
|
16
|
+
Client.request(:get, "/webhooks/#{Client.path_escape(webhook_id)}")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# GET /webhooks
|
|
20
|
+
def list(params = {})
|
|
21
|
+
Client.request(:get, "/webhooks", query: Client.pagination(params))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# PATCH /webhooks/:id — params: { endpoint:, events:, status: }
|
|
25
|
+
def update(webhook_id, params)
|
|
26
|
+
Client.request(:patch, "/webhooks/#{Client.path_escape(webhook_id)}", body: params)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Rotate the signing secret; the new plaintext secret is returned once and
|
|
30
|
+
# the old one stops verifying immediately. POST /webhooks/:id/rotate
|
|
31
|
+
def rotate(webhook_id)
|
|
32
|
+
Client.request(:post, "/webhooks/#{Client.path_escape(webhook_id)}/rotate")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Send a synchronous test delivery and return the endpoint's live result.
|
|
36
|
+
# POST /webhooks/:id/test
|
|
37
|
+
def test(webhook_id)
|
|
38
|
+
Client.request(:post, "/webhooks/#{Client.path_escape(webhook_id)}/test")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# DELETE /webhooks/:id
|
|
42
|
+
def delete(webhook_id)
|
|
43
|
+
Client.request(:delete, "/webhooks/#{Client.path_escape(webhook_id)}")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Verify a webhook delivery's Svix-style signature against your endpoint's
|
|
47
|
+
# signing secret. Pure local computation (OpenSSL HMAC-SHA256) — no HTTP.
|
|
48
|
+
#
|
|
49
|
+
# `payload` MUST be the exact raw request body string (do not re-serialize
|
|
50
|
+
# parsed JSON). `headers` is a Hash carrying svix-id / svix-timestamp /
|
|
51
|
+
# svix-signature (read case-insensitively; array values use the first
|
|
52
|
+
# element). The signature header may carry multiple space-separated
|
|
53
|
+
# `v1,<base64>` entries — any one match makes the delivery valid.
|
|
54
|
+
#
|
|
55
|
+
# Returns { valid: true } or { valid: false, reason: "..." }.
|
|
56
|
+
#
|
|
57
|
+
# result = Mailblastr::Webhooks.verify_signature(request.raw_post, request.headers.to_h, secret)
|
|
58
|
+
# head :unauthorized unless result[:valid]
|
|
59
|
+
def verify_signature(payload, headers, secret, tolerance: 300)
|
|
60
|
+
id = read_header(headers, "svix-id")
|
|
61
|
+
timestamp = read_header(headers, "svix-timestamp")
|
|
62
|
+
sig_header = read_header(headers, "svix-signature")
|
|
63
|
+
return { valid: false, reason: "missing_headers" } if id.nil? || timestamp.nil? || sig_header.nil?
|
|
64
|
+
return { valid: false, reason: "missing_secret" } if secret.nil? || secret.to_s.empty?
|
|
65
|
+
|
|
66
|
+
# Optional timestamp freshness check (default 5 minutes; 0 disables).
|
|
67
|
+
if tolerance && tolerance.positive?
|
|
68
|
+
ts = begin
|
|
69
|
+
Integer(timestamp, 10)
|
|
70
|
+
rescue ArgumentError, TypeError
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
return { valid: false, reason: "invalid_timestamp" } if ts.nil?
|
|
74
|
+
return { valid: false, reason: "timestamp_out_of_tolerance" } if (Time.now.to_i - ts).abs > tolerance
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
signed = "#{id}.#{timestamp}.#{payload}"
|
|
78
|
+
digest = OpenSSL::HMAC.digest("SHA256", secret_to_key(secret), signed)
|
|
79
|
+
expected = [digest].pack("m0") # strict base64, no newline
|
|
80
|
+
|
|
81
|
+
sig_header.split(" ").each do |part|
|
|
82
|
+
part = part.strip
|
|
83
|
+
next if part.empty?
|
|
84
|
+
|
|
85
|
+
sig = part.start_with?("v1,") ? part[3..] : part
|
|
86
|
+
return { valid: true } if secure_compare(sig, expected)
|
|
87
|
+
end
|
|
88
|
+
{ valid: false, reason: "no_match" }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
# Case-insensitively read one header value (first element if an array).
|
|
94
|
+
def read_header(headers, name)
|
|
95
|
+
return nil unless headers.is_a?(Hash)
|
|
96
|
+
|
|
97
|
+
lower = name.downcase
|
|
98
|
+
headers.each do |k, v|
|
|
99
|
+
next unless k.to_s.downcase == lower
|
|
100
|
+
|
|
101
|
+
return v.is_a?(Array) ? v.first : v
|
|
102
|
+
end
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Derive the HMAC key from a `whsec_`-prefixed secret (base64-decode the
|
|
107
|
+
# suffix); a secret without the prefix is used as raw UTF-8 bytes.
|
|
108
|
+
def secret_to_key(secret)
|
|
109
|
+
s = secret.to_s
|
|
110
|
+
if s.start_with?("whsec_")
|
|
111
|
+
decoded = s["whsec_".length..].unpack1("m") # lenient base64 decode
|
|
112
|
+
return decoded if decoded && !decoded.empty?
|
|
113
|
+
end
|
|
114
|
+
s
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Constant-time compare of two signature strings.
|
|
118
|
+
def secure_compare(a, b)
|
|
119
|
+
a = a.to_s
|
|
120
|
+
b = b.to_s
|
|
121
|
+
return false unless a.bytesize == b.bytesize
|
|
122
|
+
|
|
123
|
+
l = a.unpack("C*")
|
|
124
|
+
res = 0
|
|
125
|
+
b.each_byte.with_index { |byte, i| res |= byte ^ l[i] }
|
|
126
|
+
res.zero?
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
data/lib/mailblastr.rb
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mailblastr/version"
|
|
4
|
+
require "mailblastr/error"
|
|
5
|
+
require "mailblastr/client"
|
|
6
|
+
require "mailblastr/emails"
|
|
7
|
+
require "mailblastr/domains"
|
|
8
|
+
require "mailblastr/audiences"
|
|
9
|
+
require "mailblastr/contacts"
|
|
10
|
+
require "mailblastr/contact_properties"
|
|
11
|
+
require "mailblastr/segments"
|
|
12
|
+
require "mailblastr/topics"
|
|
13
|
+
require "mailblastr/campaigns"
|
|
14
|
+
require "mailblastr/templates"
|
|
15
|
+
require "mailblastr/automations"
|
|
16
|
+
require "mailblastr/webhooks"
|
|
17
|
+
require "mailblastr/events"
|
|
18
|
+
require "mailblastr/api_keys"
|
|
19
|
+
require "mailblastr/logs"
|
|
20
|
+
require "mailblastr/polls"
|
|
21
|
+
|
|
22
|
+
# The MailBlastr API client.
|
|
23
|
+
#
|
|
24
|
+
# Mailblastr.api_key = "mb_xxxxxxxxx"
|
|
25
|
+
#
|
|
26
|
+
# sent = Mailblastr::Emails.send({
|
|
27
|
+
# from: "Acme <hello@yourdomain.com>",
|
|
28
|
+
# to: ["user@example.com"],
|
|
29
|
+
# subject: "Hello from MailBlastr",
|
|
30
|
+
# html: "<p>Your first email</p>"
|
|
31
|
+
# })
|
|
32
|
+
# sent["id"] # => "..."
|
|
33
|
+
module Mailblastr
|
|
34
|
+
DEFAULT_BASE_URL = "https://www.mailblastr.com/api"
|
|
35
|
+
|
|
36
|
+
# Per-request network timeout, in seconds, applied to both the connect
|
|
37
|
+
# (open) and read phases. 0 or nil means "no timeout".
|
|
38
|
+
DEFAULT_TIMEOUT = 30
|
|
39
|
+
|
|
40
|
+
# How many times a retryable response (HTTP 429/503) is retried before
|
|
41
|
+
# giving up. 0 disables retries (a single attempt).
|
|
42
|
+
DEFAULT_MAX_RETRIES = 2
|
|
43
|
+
|
|
44
|
+
class << self
|
|
45
|
+
# Your API key, e.g. "mb_xxxxxxxxx". Required before any call.
|
|
46
|
+
attr_accessor :api_key
|
|
47
|
+
|
|
48
|
+
# Override the API host (defaults to https://www.mailblastr.com/api).
|
|
49
|
+
attr_writer :base_url
|
|
50
|
+
|
|
51
|
+
# Per-request timeout in seconds (default 30). Set 0 (or nil) for none.
|
|
52
|
+
attr_writer :timeout
|
|
53
|
+
|
|
54
|
+
# Max automatic retries on HTTP 429/503 (default 2, i.e. up to 3 tries).
|
|
55
|
+
attr_writer :max_retries
|
|
56
|
+
|
|
57
|
+
def base_url
|
|
58
|
+
@base_url || DEFAULT_BASE_URL
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def timeout
|
|
62
|
+
@timeout.nil? ? DEFAULT_TIMEOUT : @timeout
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def max_retries
|
|
66
|
+
@max_retries.nil? ? DEFAULT_MAX_RETRIES : @max_retries
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Mailblastr.configure do |config|
|
|
70
|
+
# config.api_key = ENV["MAILBLASTR_API_KEY"]
|
|
71
|
+
# end
|
|
72
|
+
def configure
|
|
73
|
+
yield self
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mailblastr
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- MailBlastr
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-24 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: minitest
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '5.0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '5.0'
|
|
27
|
+
description: 'Send transactional and marketing email from your own verified domain:
|
|
28
|
+
emails, domains, contacts, segments, campaigns, templates, automations, webhooks,
|
|
29
|
+
events and more. Zero runtime dependencies (Net::HTTP only).'
|
|
30
|
+
email:
|
|
31
|
+
- support@mailblastr.com
|
|
32
|
+
executables: []
|
|
33
|
+
extensions: []
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- LICENSE
|
|
37
|
+
- README.md
|
|
38
|
+
- lib/mailblastr.rb
|
|
39
|
+
- lib/mailblastr/api_keys.rb
|
|
40
|
+
- lib/mailblastr/audiences.rb
|
|
41
|
+
- lib/mailblastr/automations.rb
|
|
42
|
+
- lib/mailblastr/campaigns.rb
|
|
43
|
+
- lib/mailblastr/client.rb
|
|
44
|
+
- lib/mailblastr/contact_properties.rb
|
|
45
|
+
- lib/mailblastr/contacts.rb
|
|
46
|
+
- lib/mailblastr/domains.rb
|
|
47
|
+
- lib/mailblastr/emails.rb
|
|
48
|
+
- lib/mailblastr/error.rb
|
|
49
|
+
- lib/mailblastr/events.rb
|
|
50
|
+
- lib/mailblastr/logs.rb
|
|
51
|
+
- lib/mailblastr/polls.rb
|
|
52
|
+
- lib/mailblastr/segments.rb
|
|
53
|
+
- lib/mailblastr/templates.rb
|
|
54
|
+
- lib/mailblastr/topics.rb
|
|
55
|
+
- lib/mailblastr/version.rb
|
|
56
|
+
- lib/mailblastr/webhooks.rb
|
|
57
|
+
homepage: https://www.mailblastr.com
|
|
58
|
+
licenses:
|
|
59
|
+
- MIT
|
|
60
|
+
metadata:
|
|
61
|
+
homepage_uri: https://www.mailblastr.com
|
|
62
|
+
documentation_uri: https://www.mailblastr.com/docs
|
|
63
|
+
rubygems_mfa_required: 'true'
|
|
64
|
+
post_install_message:
|
|
65
|
+
rdoc_options: []
|
|
66
|
+
require_paths:
|
|
67
|
+
- lib
|
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '2.7'
|
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '0'
|
|
78
|
+
requirements: []
|
|
79
|
+
rubygems_version: 3.5.22
|
|
80
|
+
signing_key:
|
|
81
|
+
specification_version: 4
|
|
82
|
+
summary: Official Ruby SDK for the MailBlastr email API
|
|
83
|
+
test_files: []
|