azure_communication_email 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f6425a9b60979a7f4d2388fd367a092ee2f63ffa90c087ab71e923bb8944419f
4
+ data.tar.gz: a455a2ec63a22d96849d102a0af4860633c76720ce85d6b35d20b9fa68461c96
5
+ SHA512:
6
+ metadata.gz: df8bb2c7678d15227d6b50d3ecc950cf8984a05ef71f870e82dd0fc8f6bce24439fd59d291a684d280e3a61e8f987579675a9d939bf6a49deda8971e4774880b
7
+ data.tar.gz: 7ca782265ac31312825f7fbc78cac03225266e9edfb55cd8e9ca6d62012f780a2462d2c254e91c3748fb4dd10bebe1fc74935a5c88a1d271f97c1e6e1f93ca65
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Devran Cosmo Uenal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # Azure Communication Email
2
+
3
+ Azure Communication Email is an Action Mailer delivery method for Ruby on Rails using the Azure Communication Email API.
4
+
5
+ ## Installation
6
+
7
+ Install the gem and add to your application's Gemfile by executing:
8
+
9
+ ```bash
10
+ bundle add azure_communication_email
11
+ ```
12
+
13
+ Or add it manually to your Gemfile:
14
+
15
+ ```ruby
16
+ gem "azure_communication_email"
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ To send emails using Azure Communication Services, configure Action Mailer with the `:azure_communication_email` delivery method and provide the necessary credentials.
22
+
23
+ ```ruby
24
+ # config/environments/production.rb
25
+
26
+ Rails.application.configure do
27
+ config.action_mailer.delivery_method = :azure_communication_email
28
+ config.action_mailer.azure_communication_email_settings = {
29
+ endpoint: ENV.fetch("ACS_EMAIL_ENDPOINT"),
30
+ access_key: ENV.fetch("ACS_EMAIL_ACCESS_KEY"),
31
+ }
32
+ end
33
+ ```
34
+
35
+ ## Contributing
36
+
37
+ Bug reports and pull requests are welcome.
38
+
39
+ ## License
40
+
41
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "payload"
4
+ require_relative "hmac_auth"
5
+ require_relative "error"
6
+
7
+ require "active_support/all"
8
+ require "net/http"
9
+ require "uri"
10
+
11
+ module AzureCommunicationEmail
12
+ class DeliveryMethod
13
+ DEFAULTS = { api_version: "2023-03-31" }
14
+
15
+ attr_accessor :endpoint, :api_version, :access_key
16
+
17
+ def initialize(values)
18
+ @endpoint = values.fetch(:endpoint)
19
+ @api_version = values.fetch(:api_version, DEFAULTS[:api_version])
20
+ @access_key = values.fetch(:access_key)
21
+ end
22
+
23
+ def deliver!(mail)
24
+ raise ArgumentError, "Missing :endpoint configuration (https://my-resource.communication.azure.com)" if @endpoint.blank?
25
+ raise ArgumentError, "Missing :access_key configuration" if @access_key.blank?
26
+
27
+ path_and_query = "/emails:send?api-version=#{@api_version}"
28
+ uri = URI.join(@endpoint, path_and_query)
29
+
30
+ # Prepare email payload
31
+ payload = Payload.new(mail)
32
+ body_json = payload.to_json
33
+
34
+ # Sign request
35
+ hmac_auth = HmacAuth.new(endpoint: @endpoint, access_key: @access_key)
36
+ headers = hmac_auth.sign_request(
37
+ http_method: "POST",
38
+ path_and_query: path_and_query,
39
+ body: body_json
40
+ )
41
+
42
+ # Azure Communication Services Email
43
+ http = Net::HTTP.new(uri.host, uri.port)
44
+ http.use_ssl = (uri.scheme == "https")
45
+ http.open_timeout = 5
46
+ http.read_timeout = 15
47
+ request = Net::HTTP::Post.new(uri.request_uri, headers)
48
+ request.body = body_json
49
+
50
+ response = http.request(request)
51
+ unless response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPAccepted)
52
+ raise Error, "Failed to send email: #{response.code} #{response.message} - #{response.body}"
53
+ end
54
+
55
+ response
56
+ rescue StandardError => e
57
+ raise Error, "Error sending email: #{e.message}"
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AzureCommunicationEmail
4
+ class Error < StandardError; end
5
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "openssl"
5
+ require "base64"
6
+ require "time"
7
+
8
+ # HMAC authentication for Azure Communication Services Email REST API
9
+ #
10
+ # Reference:
11
+ # https://learn.microsoft.com/en-us/azure/communication-services/tutorials/hmac-header-tutorial?pivots=programming-language-python
12
+
13
+ module AzureCommunicationEmail
14
+ class HmacAuth
15
+ def initialize(endpoint:, access_key:)
16
+ @endpoint = endpoint # e.g. "https://my-resource.communication.azure.com"
17
+ @access_key = access_key
18
+ end
19
+
20
+ # body: must be the exact JSON string that will go in request.body
21
+ def sign_request(http_method:, path_and_query:, body:)
22
+ uri = URI.join(@endpoint, path_and_query)
23
+
24
+ content_bytes = body.encode("utf-8")
25
+
26
+ # RFC1123 UTC timestamp
27
+ date = Time.now.utc.httpdate
28
+
29
+ # Base64(SHA256(request-body-bytes))
30
+ content_hash = base64_sha256(content_bytes)
31
+
32
+ # StringToSign
33
+ host = uri.host.downcase
34
+ string_to_sign = [
35
+ http_method.upcase,
36
+ path_and_query,
37
+ "#{date};#{host};#{content_hash}"
38
+ ].join("\n")
39
+
40
+ signature = base64_hmac_sha256(string_to_sign, @access_key)
41
+
42
+ authorization_header = "HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=#{signature}"
43
+
44
+ {
45
+ "x-ms-date" => date,
46
+ "x-ms-content-sha256" => content_hash,
47
+ "Authorization" => authorization_header,
48
+ "Content-Type" => "application/json"
49
+ }
50
+ end
51
+
52
+ private
53
+
54
+ def base64_sha256(bytes)
55
+ digest = OpenSSL::Digest::SHA256.digest(bytes)
56
+ Base64.strict_encode64(digest)
57
+ end
58
+
59
+ def base64_hmac_sha256(string_to_sign, base64_secret)
60
+ secret = Base64.decode64(base64_secret)
61
+ hmac = OpenSSL::HMAC.digest("sha256", secret, string_to_sign.encode("utf-8"))
62
+ Base64.strict_encode64(hmac)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/all"
4
+ require "json"
5
+ require "base64"
6
+
7
+ module AzureCommunicationEmail
8
+ class Payload
9
+ def initialize(mail)
10
+ @mail = mail
11
+ end
12
+
13
+ def as_json
14
+ hash = {
15
+ "senderAddress" => first_address(@mail.from),
16
+ "content" => {
17
+ "subject" => @mail.subject.to_s,
18
+ "plainText" => plain_text_body
19
+ },
20
+ "recipients" => {
21
+ "to" => recipient_objects(@mail.to)
22
+ }
23
+ }
24
+
25
+ html = html_body
26
+ hash["content"]["html"] = html if html.present?
27
+
28
+ cc = recipient_objects(@mail.cc)
29
+ bcc = recipient_objects(@mail.bcc)
30
+ hash["recipients"]["cc"] = cc if cc.present?
31
+ hash["recipients"]["bcc"] = bcc if bcc.present?
32
+
33
+ reply_to = recipient_objects(@mail.reply_to)
34
+ hash["replyTo"] = reply_to if reply_to.present?
35
+
36
+ headers = custom_headers
37
+ hash["headers"] = headers if headers.present?
38
+
39
+ attachments = attachment_objects
40
+ hash["attachments"] = attachments if attachments.present?
41
+
42
+ hash
43
+ end
44
+
45
+ def to_json(*args)
46
+ JSON.generate(as_json, *args)
47
+ end
48
+
49
+ private
50
+
51
+ def plain_text_body
52
+ if @mail.text_part
53
+ @mail.text_part.decoded
54
+ else
55
+ @mail.body.decoded.to_s
56
+ end
57
+ end
58
+
59
+ def html_body
60
+ @mail.html_part&.decoded
61
+ end
62
+
63
+ def first_address(list)
64
+ Array(list).compact_blank.first
65
+ end
66
+
67
+ def recipient_objects(addresses)
68
+ Array(addresses).compact_blank.map { |address| { "address" => address } }
69
+ end
70
+
71
+ def attachment_objects
72
+ return [] unless @mail.attachments&.any?
73
+
74
+ @mail.attachments.map do |attachment|
75
+ {
76
+ "name" => attachment.filename.to_s,
77
+ "contentType" => attachment.mime_type.to_s,
78
+ "contentInBase64" => Base64.strict_encode64(attachment.body.decoded)
79
+ }
80
+ end
81
+ end
82
+
83
+ def custom_headers
84
+ return {} unless @mail.header
85
+
86
+ @mail.header.fields
87
+ .select { |field| field.name =~ /\AX-/i }
88
+ .to_h { |field| [ field.name, field.value.to_s ] }
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AzureCommunicationEmail
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "azure_communication_email/version"
4
+ require_relative "azure_communication_email/delivery_method"
5
+
6
+ # Auto-register with Rails when loaded
7
+ if defined?(Rails) && defined?(ActionMailer)
8
+ ActionMailer::Base.add_delivery_method :azure_communication_email, AzureCommunicationEmail::DeliveryMethod
9
+ end
@@ -0,0 +1,4 @@
1
+ module AzureCommunicationEmail
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: azure_communication_email
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Devran Cosmo Uenal
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: base64
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 0.2.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 0.2.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: activesupport
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '7.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '7.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: actionmailer
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '7.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '7.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: mail
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '2.8'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '2.8'
68
+ description: Action Mailer delivery method using the Azure Communication Services
69
+ Email API.
70
+ email:
71
+ - maccosmo@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - LICENSE.txt
77
+ - README.md
78
+ - Rakefile
79
+ - lib/azure_communication_email.rb
80
+ - lib/azure_communication_email/delivery_method.rb
81
+ - lib/azure_communication_email/error.rb
82
+ - lib/azure_communication_email/hmac_auth.rb
83
+ - lib/azure_communication_email/payload.rb
84
+ - lib/azure_communication_email/version.rb
85
+ - sig/azure_communication_email.rbs
86
+ homepage: https://github.com/Cosmo/azure_communication_email
87
+ licenses:
88
+ - MIT
89
+ metadata:
90
+ homepage_uri: https://github.com/Cosmo/azure_communication_email
91
+ source_code_uri: https://github.com/Cosmo/azure_communication_email
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: 3.2.0
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ requirements: []
106
+ rubygems_version: 3.7.1
107
+ specification_version: 4
108
+ summary: Azure Communication Services Email Delivery Method for Action Mailer
109
+ test_files: []