ms-graph-mailer 0.1.0 → 1.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 +4 -4
- data/CHANGELOG.md +38 -5
- data/README.md +18 -0
- data/lib/ms/graph/mailer/delivery_method.rb +40 -13
- data/lib/ms/graph/mailer/errors.rb +62 -0
- data/lib/ms/graph/mailer/version.rb +1 -1
- data/lib/ms/graph/mailer.rb +7 -6
- metadata +19 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 469b5d7a57ff7c226a55b9e73cd2995ee229f9f22824e9155585d905c66b5887
|
|
4
|
+
data.tar.gz: 4a358f5c1ea87954bfa99169a41faa6f226d2fc80a054dede48ced754b32a6ac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fa851676f45ced636be7c356cf7f33dc36ecc2685d9fb6662692756b0fdcae2fbb908f30b5b9908c08a88192c2f1858e78b1bf23d982e6034b9e25b3373245c7
|
|
7
|
+
data.tar.gz: 2c9a35d08b4579a65bd9c1596d0074cf21368f596c0a5a18362d4136e40981a1e521ad89a88b6bf25f5a6e8059ae9e67149c9b72cccdffdabcd1be6e78afa9ed
|
data/CHANGELOG.md
CHANGED
|
@@ -5,13 +5,47 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.1.0] - 2026-11-06
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `MsGraphMailer::DeliveryError` exposes `status`, `code`, `retry_after`, `request_id` and
|
|
13
|
+
`client_request_id`, plus `throttled?` (429/503 — rejected, safe to retry) and
|
|
14
|
+
`outcome_unknown?` (other 5xx and network errors — the mail may have been sent).
|
|
15
|
+
- Every sendMail request carries a generated `client-request-id`; request ids and
|
|
16
|
+
`Retry-After` are logged on success and failure.
|
|
17
|
+
- `open_timeout` / `timeout` configuration options for the sendMail request.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Error classes moved to `lib/ms/graph/mailer/errors.rb`. Error messages are unchanged.
|
|
22
|
+
|
|
23
|
+
## [1.0.0] - 2025-12-15
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- Automated CI/CD workflow with GitHub Actions
|
|
28
|
+
- Continuous Integration testing across Ruby 2.7, 3.0, 3.1, 3.2
|
|
29
|
+
- Automated releases using Trusted Publishing (no API keys needed)
|
|
30
|
+
- Version bump script (`scripts/bump_version.rb`)
|
|
31
|
+
- Comprehensive deployment documentation
|
|
32
|
+
- Rake tasks for version bumping (`rake version:major/minor/patch`)
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- **BREAKING**: Renamed gem from `microsoft-graph-mailer` to `ms-graph-mailer`
|
|
37
|
+
- **BREAKING**: Changed module namespace from `MicrosoftGraphMailer` to `MsGraphMailer`
|
|
38
|
+
- **BREAKING**: Changed require path from `microsoft/graph/mailer` to `ms/graph/mailer`
|
|
39
|
+
- Improved RuboCop configuration
|
|
40
|
+
- Enhanced CHANGELOG format with proper version links
|
|
41
|
+
|
|
8
42
|
## [0.1.0] - 2025-12-15
|
|
9
43
|
|
|
10
44
|
### Added
|
|
11
45
|
|
|
12
46
|
- Initial release of ms-graph-mailer gem
|
|
13
47
|
- ActionMailer delivery method for Microsoft Graph API
|
|
14
|
-
- OAuth 2.0 client credentials flow authentication
|
|
48
|
+
- OAuth 2.0 client credentials flow authentication with token caching
|
|
15
49
|
- Support for HTML and plain text emails
|
|
16
50
|
- Support for attachments (file attachments)
|
|
17
51
|
- Support for CC, BCC, and Reply-To recipients
|
|
@@ -19,12 +53,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
19
53
|
- Configurable SSL verification
|
|
20
54
|
- Logging support
|
|
21
55
|
- Complete documentation and examples
|
|
22
|
-
|
|
23
|
-
### Features
|
|
24
|
-
|
|
25
56
|
- `MsGraphMailer::DeliveryMethod` - Main delivery method class
|
|
26
|
-
- `MsGraphMailer::TokenService` - OAuth token management
|
|
57
|
+
- `MsGraphMailer::TokenService` - OAuth token management with Rails.cache
|
|
27
58
|
- Configuration system for tenant, client credentials
|
|
28
59
|
- Integration with Rails logger
|
|
29
60
|
|
|
61
|
+
[1.1.0]: https://github.com/zauberware/ms-graph-mailer/releases/tag/v1.1.0
|
|
62
|
+
[1.0.0]: https://github.com/zauberware/ms-graph-mailer/releases/tag/v1.0.0
|
|
30
63
|
[0.1.0]: https://github.com/zauberware/ms-graph-mailer/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -48,6 +48,9 @@ MsGraphMailer.configure do |config|
|
|
|
48
48
|
config.client_id = ENV['AZURE_MAIL_APP_CLIENT_ID']
|
|
49
49
|
config.client_secret = ENV['AZURE_MAIL_APP_CLIENT_SECRET']
|
|
50
50
|
config.logger = Rails.logger
|
|
51
|
+
# Optional: request timeouts in seconds (default: HTTP adapter defaults)
|
|
52
|
+
config.open_timeout = 10
|
|
53
|
+
config.timeout = 120
|
|
51
54
|
end
|
|
52
55
|
```
|
|
53
56
|
|
|
@@ -106,6 +109,21 @@ The gem raises specific exceptions for different error scenarios:
|
|
|
106
109
|
- `MsGraphMailer::AuthenticationError` - OAuth authentication failures
|
|
107
110
|
- `MsGraphMailer::DeliveryError` - Email delivery failures
|
|
108
111
|
|
|
112
|
+
`DeliveryError` carries the Graph response details so callers can decide whether to retry:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
rescue MsGraphMailer::DeliveryError => e
|
|
116
|
+
e.status # => 503
|
|
117
|
+
e.code # => "CommandConcurrencyLimitReached"
|
|
118
|
+
e.retry_after # => 7 (seconds, from the Retry-After header; nil if absent)
|
|
119
|
+
e.request_id # => Graph request-id, for Microsoft support
|
|
120
|
+
e.client_request_id # => id the gem sent with the request (also set on timeouts)
|
|
121
|
+
|
|
122
|
+
e.throttled? # 429/503: Graph rejected the request, nothing was sent - safe to retry
|
|
123
|
+
e.outcome_unknown? # other 5xx or network error/timeout - the mail may have been sent
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
109
127
|
## Development
|
|
110
128
|
|
|
111
129
|
After checking out the repo, run:
|
|
@@ -28,31 +28,56 @@ module MsGraphMailer
|
|
|
28
28
|
saveToSentItems: true
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
# Sent with the request and echoed back by Graph, so a failure can be
|
|
32
|
+
# traced with Microsoft even when no response arrives (timeouts).
|
|
33
|
+
client_request_id = SecureRandom.uuid
|
|
34
|
+
|
|
31
35
|
response = connection(token_result.value!).post("users/#{CGI.escape(sender_email)}/sendMail") do |req|
|
|
32
36
|
req.headers['Content-Type'] = 'application/json'
|
|
37
|
+
req.headers['client-request-id'] = client_request_id
|
|
38
|
+
req.headers['return-client-request-id'] = 'true'
|
|
33
39
|
req.body = message_payload.to_json
|
|
34
40
|
end
|
|
35
41
|
|
|
36
|
-
unless response.status == 202
|
|
37
|
-
error_body = begin
|
|
38
|
-
JSON.parse(response.body)
|
|
39
|
-
rescue StandardError
|
|
40
|
-
{}
|
|
41
|
-
end
|
|
42
|
-
error_msg = error_body.dig('error', 'message') || 'Unknown error'
|
|
43
|
-
logger.error("[MsGraphMailer] sendMail failed: [status: #{response.status}] - [body: #{response.body}]")
|
|
44
|
-
raise DeliveryError, "Microsoft Graph sendEmail failed with status #{response.status}: #{error_msg}"
|
|
45
|
-
end
|
|
42
|
+
raise build_delivery_error(response, client_request_id) unless response.status == 202
|
|
46
43
|
|
|
47
|
-
logger.info('[MsGraphMailer] Email sent successfully via Microsoft Graph'
|
|
44
|
+
logger.info('[MsGraphMailer] Email sent successfully via Microsoft Graph ' \
|
|
45
|
+
"[request-id: #{response.headers['request-id']}] [client-request-id: #{client_request_id}]")
|
|
48
46
|
response
|
|
49
47
|
rescue Faraday::Error => e
|
|
50
|
-
logger.error("[MsGraphMailer] Network error: #{e.message}")
|
|
51
|
-
raise DeliveryError
|
|
48
|
+
logger.error("[MsGraphMailer] Network error: #{e.message} [client-request-id: #{client_request_id}]")
|
|
49
|
+
raise DeliveryError.new("Network error sending email: #{e.message}",
|
|
50
|
+
client_request_id: client_request_id, network_error: true)
|
|
52
51
|
end
|
|
53
52
|
|
|
54
53
|
private
|
|
55
54
|
|
|
55
|
+
def build_delivery_error(response, client_request_id)
|
|
56
|
+
error = parse_error_body(response.body)
|
|
57
|
+
request_id = response.headers['request-id']
|
|
58
|
+
retry_after = DeliveryError.parse_retry_after(response.headers['Retry-After'])
|
|
59
|
+
|
|
60
|
+
logger.error("[MsGraphMailer] sendMail failed: [status: #{response.status}] [code: #{error['code']}] " \
|
|
61
|
+
"[request-id: #{request_id}] [client-request-id: #{client_request_id}] " \
|
|
62
|
+
"[retry-after: #{retry_after.inspect}] - [body: #{response.body}]")
|
|
63
|
+
|
|
64
|
+
DeliveryError.new(
|
|
65
|
+
"Microsoft Graph sendEmail failed with status #{response.status}: #{error['message'] || 'Unknown error'}",
|
|
66
|
+
status: response.status,
|
|
67
|
+
code: error['code'],
|
|
68
|
+
retry_after: retry_after,
|
|
69
|
+
request_id: request_id,
|
|
70
|
+
client_request_id: client_request_id
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def parse_error_body(body)
|
|
75
|
+
parsed = JSON.parse(body.to_s)
|
|
76
|
+
parsed.is_a?(Hash) && parsed['error'].is_a?(Hash) ? parsed['error'] : {}
|
|
77
|
+
rescue JSON::ParserError
|
|
78
|
+
{}
|
|
79
|
+
end
|
|
80
|
+
|
|
56
81
|
def validate_configuration!
|
|
57
82
|
config = MsGraphMailer.configuration
|
|
58
83
|
return if config&.valid?
|
|
@@ -133,6 +158,8 @@ module MsGraphMailer
|
|
|
133
158
|
def connection(access_token)
|
|
134
159
|
@connection ||= Faraday.new(url: 'https://graph.microsoft.com/v1.0/') do |f|
|
|
135
160
|
f.headers['Authorization'] = "Bearer #{access_token}"
|
|
161
|
+
f.options.open_timeout = MsGraphMailer.configuration&.open_timeout
|
|
162
|
+
f.options.timeout = MsGraphMailer.configuration&.timeout
|
|
136
163
|
f.adapter Faraday.default_adapter
|
|
137
164
|
# SSL verification configuration
|
|
138
165
|
f.ssl.verify = ssl_verify?
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
module MsGraphMailer
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
class AuthenticationError < Error; end
|
|
8
|
+
class ConfigurationError < Error; end
|
|
9
|
+
|
|
10
|
+
# Raised when Microsoft Graph does not accept a sendMail request.
|
|
11
|
+
#
|
|
12
|
+
# Besides the message (kept stable for callers that persist or match it),
|
|
13
|
+
# the error exposes the response details a caller needs to decide whether a
|
|
14
|
+
# retry is safe:
|
|
15
|
+
#
|
|
16
|
+
# - throttled? Graph rejected the request (429, or 503 such as
|
|
17
|
+
# "CommandConcurrencyLimitReached"). Nothing was sent;
|
|
18
|
+
# retry after #retry_after seconds.
|
|
19
|
+
# - outcome_unknown? Gateway errors and network failures/timeouts. The
|
|
20
|
+
# mail may or may not have been sent — retrying blindly
|
|
21
|
+
# risks a duplicate.
|
|
22
|
+
class DeliveryError < Error
|
|
23
|
+
THROTTLED_STATUSES = [429, 503].freeze
|
|
24
|
+
|
|
25
|
+
attr_reader :status, :code, :retry_after, :request_id, :client_request_id
|
|
26
|
+
|
|
27
|
+
def initialize(message = nil, status: nil, code: nil, retry_after: nil, request_id: nil, # rubocop:disable Metrics/ParameterLists
|
|
28
|
+
client_request_id: nil, network_error: false)
|
|
29
|
+
super(message)
|
|
30
|
+
@status = status
|
|
31
|
+
@code = code
|
|
32
|
+
@retry_after = retry_after
|
|
33
|
+
@request_id = request_id
|
|
34
|
+
@client_request_id = client_request_id
|
|
35
|
+
@network_error = network_error
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def throttled?
|
|
39
|
+
THROTTLED_STATUSES.include?(status)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def outcome_unknown?
|
|
43
|
+
return true if @network_error
|
|
44
|
+
return false if status.nil? || throttled?
|
|
45
|
+
|
|
46
|
+
status >= 500
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Retry-After is either delta-seconds or an HTTP date (RFC 9110 §10.2.3).
|
|
50
|
+
# Returns whole seconds, or nil when the header is absent or unparsable.
|
|
51
|
+
def self.parse_retry_after(value, now: Time.now)
|
|
52
|
+
return nil if value.nil? || value.to_s.strip.empty?
|
|
53
|
+
|
|
54
|
+
raw = value.to_s.strip
|
|
55
|
+
return raw.to_i if raw.match?(/\A\d+\z/)
|
|
56
|
+
|
|
57
|
+
[(Time.httpdate(raw) - now).ceil, 0].max
|
|
58
|
+
rescue ArgumentError
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/lib/ms/graph/mailer.rb
CHANGED
|
@@ -6,17 +6,14 @@ require 'dry/monads'
|
|
|
6
6
|
require 'json'
|
|
7
7
|
require 'base64'
|
|
8
8
|
require 'cgi'
|
|
9
|
+
require 'securerandom'
|
|
9
10
|
|
|
10
11
|
require_relative 'mailer/version'
|
|
12
|
+
require_relative 'mailer/errors'
|
|
11
13
|
require_relative 'mailer/token_service'
|
|
12
14
|
require_relative 'mailer/delivery_method'
|
|
13
15
|
|
|
14
16
|
module MsGraphMailer
|
|
15
|
-
class Error < StandardError; end
|
|
16
|
-
class AuthenticationError < Error; end
|
|
17
|
-
class DeliveryError < Error; end
|
|
18
|
-
class ConfigurationError < Error; end
|
|
19
|
-
|
|
20
17
|
class << self
|
|
21
18
|
attr_accessor :configuration
|
|
22
19
|
|
|
@@ -27,13 +24,17 @@ module MsGraphMailer
|
|
|
27
24
|
end
|
|
28
25
|
|
|
29
26
|
class Configuration
|
|
30
|
-
|
|
27
|
+
# open_timeout / timeout (seconds) apply to the sendMail request. nil keeps
|
|
28
|
+
# the HTTP adapter's defaults.
|
|
29
|
+
attr_accessor :tenant_id, :client_id, :client_secret, :logger, :open_timeout, :timeout
|
|
31
30
|
|
|
32
31
|
def initialize
|
|
33
32
|
@tenant_id = nil
|
|
34
33
|
@client_id = nil
|
|
35
34
|
@client_secret = nil
|
|
36
35
|
@logger = nil
|
|
36
|
+
@open_timeout = nil
|
|
37
|
+
@timeout = nil
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
def valid?
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ms-graph-mailer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Zauberware Technologies GmbH
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-11-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: actionmailer
|
|
@@ -66,6 +66,20 @@ dependencies:
|
|
|
66
66
|
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
68
|
version: '2.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: securerandom
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0.3'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0.3'
|
|
69
83
|
- !ruby/object:Gem::Dependency
|
|
70
84
|
name: bundler
|
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -177,6 +191,7 @@ files:
|
|
|
177
191
|
- README.md
|
|
178
192
|
- lib/ms/graph/mailer.rb
|
|
179
193
|
- lib/ms/graph/mailer/delivery_method.rb
|
|
194
|
+
- lib/ms/graph/mailer/errors.rb
|
|
180
195
|
- lib/ms/graph/mailer/token_service.rb
|
|
181
196
|
- lib/ms/graph/mailer/version.rb
|
|
182
197
|
homepage: https://github.com/zauberware/ms-graph-mailer
|
|
@@ -194,14 +209,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
194
209
|
requirements:
|
|
195
210
|
- - ">="
|
|
196
211
|
- !ruby/object:Gem::Version
|
|
197
|
-
version: 2.
|
|
212
|
+
version: 3.2.0
|
|
198
213
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
199
214
|
requirements:
|
|
200
215
|
- - ">="
|
|
201
216
|
- !ruby/object:Gem::Version
|
|
202
217
|
version: '0'
|
|
203
218
|
requirements: []
|
|
204
|
-
rubygems_version: 3.5.
|
|
219
|
+
rubygems_version: 3.5.22
|
|
205
220
|
signing_key:
|
|
206
221
|
specification_version: 4
|
|
207
222
|
summary: ActionMailer delivery method for Microsoft Graph API
|