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 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,7 @@
1
+ module SFMC
2
+ module Assets
3
+ class Asset < SFMCBase
4
+ endpoint "/asset/v1/content/assets"
5
+ end
6
+ end
7
+ end
@@ -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
@@ -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
@@ -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
@@ -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: []