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