sfmc_emailer 0.0.1

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