sfmc_emailer 0.0.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.
- checksums.yaml +7 -0
- data/lib/sfmc/assets/asset.rb +7 -0
- data/lib/sfmc/authentication.rb +25 -0
- data/lib/sfmc/contacts/contact_key.rb +24 -0
- data/lib/sfmc/errors.rb +37 -0
- data/lib/sfmc/helpers.rb +23 -0
- data/lib/sfmc/sfmc_base.rb +85 -0
- data/lib/sfmc/transactional/email.rb +51 -0
- data/lib/sfmc/transactional/send_definition.rb +40 -0
- data/lib/sfmc/triggered/email.rb +20 -0
- data/lib/sfmc_emailer.rb +9 -0
- metadata +67 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 239758da91fdc505fd377b2c9172189080ea70c8fce9c134676fce57c25ceb9f
|
4
|
+
data.tar.gz: 2acca7c2385a445ecf177714c32844edbf716af7604c99cbea348efde1b2eeca
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 83baaeca028d0e4240470505c6e2fc12d962944d834998280cdfe095c3fd16377cd74525f5525c08b406735557206a7364392f11648a8eff593eab7a5e35ad50
|
7
|
+
data.tar.gz: a88a332638e599815165adc83636fad1974db6fcc05d7ed11b674cd49ca9e67a7eb28ab3f3e9afdc2dc172a58ccc9ca6528c86db2f2e4d86bdfde9d0edf88ae3
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module SFMC
|
2
|
+
class Authentication < SFMCBase
|
3
|
+
endpoint "/v2/token"
|
4
|
+
|
5
|
+
AUTH_PARAMS = {
|
6
|
+
client_id: SFMCBase.client_id,
|
7
|
+
client_secret: SFMCBase.client_secret,
|
8
|
+
grant_type: 'client_credentials',
|
9
|
+
}.freeze
|
10
|
+
private_constant :AUTH_PARAMS
|
11
|
+
|
12
|
+
def self.set_bearer_token(refresh: false)
|
13
|
+
# Ensures the token is present & not expired
|
14
|
+
token_invalid = SFMCBase.access_token.nil? || SFMCBase.access_token_expires_at < Time.now
|
15
|
+
return unless refresh || token_invalid
|
16
|
+
|
17
|
+
set_base_uri 'auth'
|
18
|
+
response = create(nil, AUTH_PARAMS, true)
|
19
|
+
|
20
|
+
SFMCBase.access_token = response.access_token
|
21
|
+
SFMCBase.access_token_expires_at = Time.now + response.expires_in
|
22
|
+
SFMCBase.headers Authorization: "Bearer #{SFMCBase.access_token}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module SFMC
|
2
|
+
module Contacts
|
3
|
+
class ContactKey < SFMCBase
|
4
|
+
endpoint "/contacts/v1/addresses/email/search"
|
5
|
+
|
6
|
+
def self.find(emails, max = 1)
|
7
|
+
emails = [emails] unless emails.is_a? Array
|
8
|
+
|
9
|
+
params = {
|
10
|
+
channelAddressList: emails,
|
11
|
+
maximumCount: max,
|
12
|
+
}
|
13
|
+
response = create(nil, params)
|
14
|
+
|
15
|
+
response.channelAddressResponseEntities.map do |channel|
|
16
|
+
key = channel[:contactKeyDetails].first[:contactKey]
|
17
|
+
raise SFMC::Errors::NotFoundError if key.nil?
|
18
|
+
|
19
|
+
key
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/sfmc/errors.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
module SFMC
|
2
|
+
module Errors
|
3
|
+
# rubocop:disable Layout/EmptyLineBetweenDefs
|
4
|
+
class BadRequestError < StandardError; end
|
5
|
+
class UnauthorizedError < StandardError; end
|
6
|
+
class ForbiddenError < StandardError; end
|
7
|
+
class NotFoundError < StandardError; end
|
8
|
+
class UnprocessableEntityError < StandardError; end
|
9
|
+
class APIError < StandardError; end
|
10
|
+
# rubocop:enable Layout/EmptyLineBetweenDefs
|
11
|
+
|
12
|
+
def error_class(code)
|
13
|
+
case code.to_i
|
14
|
+
when 400
|
15
|
+
BadRequestError
|
16
|
+
when 401
|
17
|
+
UnauthorizedError
|
18
|
+
when 403
|
19
|
+
ForbiddenError
|
20
|
+
when 404
|
21
|
+
NotFoundError
|
22
|
+
when 422
|
23
|
+
UnprocessableEntityError
|
24
|
+
else
|
25
|
+
APIError
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def error_message(response)
|
30
|
+
error = response.parsed_response
|
31
|
+
documentation_msg = ", Documentation: #{error['documentation']}"
|
32
|
+
add_documentation = error['documentation'].present?
|
33
|
+
|
34
|
+
"#{response.code}, API error code: #{error['errorcode']}, Message: #{error['message']}" + (add_documentation ? documentation_msg : '')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/sfmc/helpers.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module SFMC::Helpers
|
2
|
+
def init(config)
|
3
|
+
@subdomain = config[:subdomain]
|
4
|
+
@client_id = config[:client_id]
|
5
|
+
@client_secret = config[:client_secret]
|
6
|
+
@default_send_classification = config[:default_send_classification]
|
7
|
+
@default_subscriber_list = config[:default_subscriber_list]
|
8
|
+
@default_data_extension = config[:default_data_extension]
|
9
|
+
@default_bcc = config[:default_bcc]
|
10
|
+
end
|
11
|
+
|
12
|
+
def set_base_uri(protocol = 'rest')
|
13
|
+
base_uri "https://#{SFMC::SFMCBase.subdomain}.#{protocol}.marketingcloudapis.com"
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_subscriber_key(email_address)
|
17
|
+
begin
|
18
|
+
SFMC::Contacts::ContactKey.find(email_address).first
|
19
|
+
rescue SFMC::Errors::NotFoundError
|
20
|
+
SecureRandom.uuid
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
|
3
|
+
module SFMC
|
4
|
+
class SFMCBase
|
5
|
+
include HTTParty
|
6
|
+
extend SFMC::Helpers
|
7
|
+
extend SFMC::Errors
|
8
|
+
|
9
|
+
format :json
|
10
|
+
|
11
|
+
NAME_TO_METHOD = {
|
12
|
+
find: :get,
|
13
|
+
create: :post,
|
14
|
+
update: :patch,
|
15
|
+
destroy: :delete,
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
class << self
|
19
|
+
# This protects against an invalid access token, for instance if the SFMC password was changed
|
20
|
+
def authenticate_and_retry(method, endpoint, params, retry_count)
|
21
|
+
SFMC::Authentication.set_bearer_token refresh: true
|
22
|
+
request method, endpoint, params, retry_count - 1
|
23
|
+
end
|
24
|
+
|
25
|
+
def request(http_method, endpoint, params = {}, retry_count = 1, is_authenticating: false)
|
26
|
+
unless is_authenticating
|
27
|
+
set_base_uri
|
28
|
+
SFMC::Authentication.set_bearer_token
|
29
|
+
end
|
30
|
+
|
31
|
+
received = method(http_method).call(endpoint, body: params, headers: SFMCBase.headers)
|
32
|
+
payload = OpenStruct.new(received.with_indifferent_access)
|
33
|
+
|
34
|
+
return payload if received.response.is_a? Net::HTTPSuccess
|
35
|
+
|
36
|
+
raise error_class(received.code), error_message(received) unless received.code == 401 && retry_count > 0
|
37
|
+
|
38
|
+
# The access token was invalidated. In this case, by default, we will retry once
|
39
|
+
authenticate_and_retry http_method, endpoint, params, retry_count
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
|
44
|
+
attr_accessor :access_token,
|
45
|
+
:access_token_expires_at,
|
46
|
+
:subdomain,
|
47
|
+
:client_id,
|
48
|
+
:client_secret,
|
49
|
+
:default_send_classification,
|
50
|
+
:default_subscriber_list,
|
51
|
+
:default_data_extension,
|
52
|
+
:default_bcc
|
53
|
+
|
54
|
+
def endpoint(path)
|
55
|
+
return if defined? @endpoint_defined
|
56
|
+
|
57
|
+
@endpoint_defined = true
|
58
|
+
extend @endpoint_module = Module.new
|
59
|
+
|
60
|
+
# Defines the CRUD methods find, create, update, and destroy
|
61
|
+
# Each method optionally accepts an id of a resource, params, and auth
|
62
|
+
# The auth param should never be used directly
|
63
|
+
@endpoint_module.module_eval do
|
64
|
+
NAME_TO_METHOD.each do |name, method|
|
65
|
+
define_method(name) do |id = nil, params = nil, auth = false|
|
66
|
+
path_with_id = path + "/#{id}"
|
67
|
+
|
68
|
+
request method, path_with_id, params, is_authenticating: auth
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Defines a resource query method
|
73
|
+
# The query param is the query to be sent,
|
74
|
+
# and the optional fields param, when set, will only return those specific fields
|
75
|
+
define_method("query") do |query, fields = nil|
|
76
|
+
query += "&$fields=#{URI::Parser.new.escape(fields)}" unless fields.nil?
|
77
|
+
full_query = query.presence ? "?$filter=#{query}" : ''
|
78
|
+
|
79
|
+
find(full_query)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module SFMC
|
2
|
+
module Transactional
|
3
|
+
class Email < SFMCBase
|
4
|
+
endpoint "/messaging/v1/email/messages"
|
5
|
+
|
6
|
+
# If an email hasn't ever been sent then we will not have an email send definition
|
7
|
+
# We're waiting a minute for the send definition to fully be initialized
|
8
|
+
# Thankfully this is a rare edge case
|
9
|
+
def self.create_email_send_definition_and_retry(name, to, params)
|
10
|
+
asset = SFMC::Assets::Asset.query("name eq '#{name}'", 'customerKey')
|
11
|
+
raise SFMC::Errors::BadRequestError, "No emails found with name #{name}" if asset.count == 0
|
12
|
+
|
13
|
+
consumer_key = asset.items.first["customerKey"]
|
14
|
+
|
15
|
+
SFMC::Transactional::SendDefinition.create(name, consumer_key)
|
16
|
+
|
17
|
+
# Even though the send def is "Active" it won't function for another minute
|
18
|
+
delay(run_at: 70.seconds.from_now).send_email(email: name, to: to, params: params, create_email_if_needed: false)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Will attempt once to create a new email send definition if one isn't found
|
22
|
+
def self.send_email(email:, to:, params: {}, create_email_if_needed: true)
|
23
|
+
subscriber_key = get_subscriber_key(to)
|
24
|
+
message_key = SecureRandom.uuid
|
25
|
+
data_extension_params = {
|
26
|
+
SubscriberKey: subscriber_key,
|
27
|
+
EmailAddress: to,
|
28
|
+
**params,
|
29
|
+
}
|
30
|
+
|
31
|
+
email_params = {
|
32
|
+
definitionKey: email,
|
33
|
+
recipient: {
|
34
|
+
contactKey: subscriber_key,
|
35
|
+
to: to,
|
36
|
+
attributes: data_extension_params,
|
37
|
+
},
|
38
|
+
}
|
39
|
+
|
40
|
+
# If SFMC::Errors::NotFoundError is raised then we need to create an email send definition
|
41
|
+
begin
|
42
|
+
create(message_key, email_params)
|
43
|
+
rescue SFMC::Errors::NotFoundError
|
44
|
+
raise unless create_email_if_needed
|
45
|
+
|
46
|
+
create_email_send_definition_and_retry(email, to, params)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module SFMC
|
2
|
+
module Transactional
|
3
|
+
class SendDefinition < SFMCBase
|
4
|
+
endpoint "/messaging/v1/email/definitions"
|
5
|
+
|
6
|
+
def self.refresh(definition_key)
|
7
|
+
update definition_key, status: 'Inactive'
|
8
|
+
update definition_key, status: 'Active'
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.create(
|
12
|
+
definition_key,
|
13
|
+
customer_key,
|
14
|
+
send_classification = nil,
|
15
|
+
subscriber_list = nil,
|
16
|
+
data_extension = nil,
|
17
|
+
bcc = []
|
18
|
+
)
|
19
|
+
params = {
|
20
|
+
classification: SFMCBase.default_send_classification || send_classification,
|
21
|
+
definitionKey: definition_key,
|
22
|
+
name: definition_key,
|
23
|
+
status: 'Active',
|
24
|
+
subscriptions: {
|
25
|
+
list: SFMCBase.default_subscriber_list || subscriber_list,
|
26
|
+
dataExtension: SFMCBase.default_data_extension || data_extension,
|
27
|
+
},
|
28
|
+
content: {
|
29
|
+
customerKey: customer_key,
|
30
|
+
},
|
31
|
+
options: {
|
32
|
+
bcc: SFMCBase.default_bcc || bcc,
|
33
|
+
},
|
34
|
+
}
|
35
|
+
|
36
|
+
super(nil, params)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module SFMC
|
2
|
+
module Triggered
|
3
|
+
class Email < SFMCBase
|
4
|
+
def self.send_email(email:, to:, params: {})
|
5
|
+
endpoint_url = "/messaging/v1/messageDefinitionSends/key:#{email}/send"
|
6
|
+
email_params = {
|
7
|
+
To: {
|
8
|
+
Address: to,
|
9
|
+
SubscriberKey: get_subscriber_key(to),
|
10
|
+
ContactAttributes: {
|
11
|
+
SubscriberAttributes: params,
|
12
|
+
},
|
13
|
+
},
|
14
|
+
}
|
15
|
+
|
16
|
+
request(:post, endpoint_url, email_params.as_json)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/sfmc_emailer.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'sfmc/errors'
|
2
|
+
require 'sfmc/helpers'
|
3
|
+
require 'sfmc/sfmc_base'
|
4
|
+
require 'sfmc/authentication'
|
5
|
+
require 'sfmc/assets/asset'
|
6
|
+
require 'sfmc/contacts/contact_key'
|
7
|
+
require 'sfmc/transactional/email'
|
8
|
+
require 'sfmc/transactional/send_definition'
|
9
|
+
require 'sfmc/triggered/email'
|
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sfmc_emailer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Angel Ruiz-Bates
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-10-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: httparty
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.20'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.20'
|
27
|
+
description: API wrapper for Salesforce Marketing Cloud's Transactional and Triggered
|
28
|
+
Send APIs
|
29
|
+
email: angeljrbt@gmail.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- lib/sfmc/assets/asset.rb
|
35
|
+
- lib/sfmc/authentication.rb
|
36
|
+
- lib/sfmc/contacts/contact_key.rb
|
37
|
+
- lib/sfmc/errors.rb
|
38
|
+
- lib/sfmc/helpers.rb
|
39
|
+
- lib/sfmc/sfmc_base.rb
|
40
|
+
- lib/sfmc/transactional/email.rb
|
41
|
+
- lib/sfmc/transactional/send_definition.rb
|
42
|
+
- lib/sfmc/triggered/email.rb
|
43
|
+
- lib/sfmc_emailer.rb
|
44
|
+
homepage: https://rubygems.org/gems/sfmc_emailer
|
45
|
+
licenses:
|
46
|
+
- MIT
|
47
|
+
metadata: {}
|
48
|
+
post_install_message:
|
49
|
+
rdoc_options: []
|
50
|
+
require_paths:
|
51
|
+
- lib
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
requirements: []
|
63
|
+
rubygems_version: 3.1.6
|
64
|
+
signing_key:
|
65
|
+
specification_version: 4
|
66
|
+
summary: API wrapper for SFMC
|
67
|
+
test_files: []
|