poodle-ruby 1.0.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 +7 -0
- data/.rspec +1 -0
- data/.rubocop.yml +65 -0
- data/.yardopts +12 -0
- data/CODE_OF_CONDUCT.md +78 -0
- data/CONTRIBUTING.md +151 -0
- data/LICENSE +21 -0
- data/README.md +442 -0
- data/Rakefile +4 -0
- data/examples/advanced_usage.rb +255 -0
- data/examples/basic_usage.rb +64 -0
- data/lib/poodle/client.rb +222 -0
- data/lib/poodle/configuration.rb +145 -0
- data/lib/poodle/email.rb +190 -0
- data/lib/poodle/email_response.rb +101 -0
- data/lib/poodle/errors/authentication_error.rb +54 -0
- data/lib/poodle/errors/base_error.rb +49 -0
- data/lib/poodle/errors/forbidden_error.rb +56 -0
- data/lib/poodle/errors/network_error.rb +104 -0
- data/lib/poodle/errors/payment_error.rb +73 -0
- data/lib/poodle/errors/rate_limit_error.rb +146 -0
- data/lib/poodle/errors/server_error.rb +57 -0
- data/lib/poodle/errors/validation_error.rb +93 -0
- data/lib/poodle/http_client.rb +327 -0
- data/lib/poodle/rails/railtie.rb +50 -0
- data/lib/poodle/rails/tasks.rake +113 -0
- data/lib/poodle/rails.rb +158 -0
- data/lib/poodle/test_helpers.rb +244 -0
- data/lib/poodle/version.rb +6 -0
- data/lib/poodle.rb +80 -0
- data/sig/poodle.rbs +4 -0
- metadata +107 -0
@@ -0,0 +1,64 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "poodle"
|
6
|
+
|
7
|
+
# Basic usage example for the Poodle Ruby SDK
|
8
|
+
|
9
|
+
# Set your API key (you can also use environment variables)
|
10
|
+
# ENV["POODLE_API_KEY"] = "your_api_key_here"
|
11
|
+
|
12
|
+
begin
|
13
|
+
# Initialize the client
|
14
|
+
client = Poodle::Client.new(api_key: "your_api_key_here")
|
15
|
+
|
16
|
+
# Send a simple HTML email
|
17
|
+
response = client.send(
|
18
|
+
from: "sender@example.com",
|
19
|
+
to: "recipient@example.com",
|
20
|
+
subject: "Hello from Poodle Ruby SDK!",
|
21
|
+
html: "<h1>Hello World!</h1><p>This email was sent using the Poodle Ruby SDK.</p>"
|
22
|
+
)
|
23
|
+
|
24
|
+
if response.success?
|
25
|
+
puts "✅ Email sent successfully!"
|
26
|
+
else
|
27
|
+
puts "❌ Failed to send email"
|
28
|
+
end
|
29
|
+
puts "Message: #{response.message}"
|
30
|
+
|
31
|
+
# Send a text-only email
|
32
|
+
text_response = client.send_text(
|
33
|
+
from: "sender@example.com",
|
34
|
+
to: "recipient@example.com",
|
35
|
+
subject: "Plain text email",
|
36
|
+
text: "This is a plain text email sent using the Poodle Ruby SDK."
|
37
|
+
)
|
38
|
+
|
39
|
+
puts "\nText email result: #{text_response.success? ? 'Success' : 'Failed'}"
|
40
|
+
|
41
|
+
# Send an HTML-only email
|
42
|
+
html_response = client.send_html(
|
43
|
+
from: "sender@example.com",
|
44
|
+
to: "recipient@example.com",
|
45
|
+
subject: "HTML email",
|
46
|
+
html: "<h2>Newsletter</h2><p>This is an HTML email with <strong>formatting</strong>.</p>"
|
47
|
+
)
|
48
|
+
|
49
|
+
puts "HTML email result: #{html_response.success? ? 'Success' : 'Failed'}"
|
50
|
+
rescue Poodle::ValidationError => e
|
51
|
+
puts "❌ Validation error: #{e.message}"
|
52
|
+
puts "Errors: #{e.errors}"
|
53
|
+
rescue Poodle::AuthenticationError => e
|
54
|
+
puts "❌ Authentication error: #{e.message}"
|
55
|
+
puts "Please check your API key"
|
56
|
+
rescue Poodle::RateLimitError => e
|
57
|
+
puts "❌ Rate limit exceeded: #{e.message}"
|
58
|
+
puts "Retry after: #{e.retry_after} seconds" if e.retry_after
|
59
|
+
rescue Poodle::Error => e
|
60
|
+
puts "❌ Poodle error: #{e.message}"
|
61
|
+
puts "Status code: #{e.status_code}" if e.status_code
|
62
|
+
rescue StandardError => e
|
63
|
+
puts "❌ Unexpected error: #{e.message}"
|
64
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "configuration"
|
4
|
+
require_relative "http_client"
|
5
|
+
require_relative "email"
|
6
|
+
require_relative "email_response"
|
7
|
+
require_relative "errors/validation_error"
|
8
|
+
|
9
|
+
module Poodle
|
10
|
+
# Main Poodle SDK client for sending emails
|
11
|
+
#
|
12
|
+
# @example Basic usage
|
13
|
+
# client = Poodle::Client.new(api_key: "your_api_key")
|
14
|
+
# response = client.send(
|
15
|
+
# from: "sender@example.com",
|
16
|
+
# to: "recipient@example.com",
|
17
|
+
# subject: "Hello World",
|
18
|
+
# html: "<h1>Hello!</h1>"
|
19
|
+
# )
|
20
|
+
#
|
21
|
+
# @example Using configuration object
|
22
|
+
# config = Poodle::Configuration.new(
|
23
|
+
# api_key: "your_api_key",
|
24
|
+
# debug: true
|
25
|
+
# )
|
26
|
+
# client = Poodle::Client.new(config)
|
27
|
+
#
|
28
|
+
# @example Using environment variables
|
29
|
+
# ENV["POODLE_API_KEY"] = "your_api_key"
|
30
|
+
# client = Poodle::Client.new
|
31
|
+
class Client
|
32
|
+
# @return [Configuration] the configuration object
|
33
|
+
attr_reader :config
|
34
|
+
|
35
|
+
# @return [HttpClient] the HTTP client
|
36
|
+
attr_reader :http_client
|
37
|
+
|
38
|
+
# Initialize a new Client
|
39
|
+
#
|
40
|
+
# @param config_or_api_key [Configuration, String, nil] configuration object or API key
|
41
|
+
# @param base_url [String, nil] base URL (only used if first param is API key)
|
42
|
+
# @param timeout [Integer, nil] request timeout (only used if first param is API key)
|
43
|
+
# @param connect_timeout [Integer, nil] connection timeout (only used if first param is API key)
|
44
|
+
# @param debug [Boolean] enable debug mode (only used if first param is API key)
|
45
|
+
# @param http_options [Hash] additional HTTP options (only used if first param is API key)
|
46
|
+
#
|
47
|
+
# @example With API key
|
48
|
+
# client = Poodle::Client.new("your_api_key")
|
49
|
+
#
|
50
|
+
# @example With configuration
|
51
|
+
# config = Poodle::Configuration.new(api_key: "your_api_key")
|
52
|
+
# client = Poodle::Client.new(config)
|
53
|
+
#
|
54
|
+
# @example With keyword arguments
|
55
|
+
# client = Poodle::Client.new(
|
56
|
+
# api_key: "your_api_key",
|
57
|
+
# debug: true,
|
58
|
+
# timeout: 60
|
59
|
+
# )
|
60
|
+
def initialize(config_or_api_key = nil, **options)
|
61
|
+
@config = case config_or_api_key
|
62
|
+
when Configuration
|
63
|
+
config_or_api_key
|
64
|
+
when String
|
65
|
+
Configuration.new(
|
66
|
+
api_key: config_or_api_key,
|
67
|
+
base_url: options[:base_url],
|
68
|
+
timeout: options[:timeout],
|
69
|
+
connect_timeout: options[:connect_timeout],
|
70
|
+
debug: options.fetch(:debug, false),
|
71
|
+
http_options: options.fetch(:http_options, {})
|
72
|
+
)
|
73
|
+
when nil
|
74
|
+
# Support keyword arguments for convenience
|
75
|
+
Configuration.new(**options)
|
76
|
+
else
|
77
|
+
raise ArgumentError, "Expected Configuration object, API key string, or nil"
|
78
|
+
end
|
79
|
+
|
80
|
+
@http_client = HttpClient.new(@config)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Send an email using an Email object or hash
|
84
|
+
#
|
85
|
+
# @param email [Email, Hash] the email to send
|
86
|
+
# @return [EmailResponse] the response from the API
|
87
|
+
# @raise [ValidationError] if email data is invalid
|
88
|
+
# @raise [Poodle::Error] if the request fails
|
89
|
+
#
|
90
|
+
# @example With Email object
|
91
|
+
# email = Poodle::Email.new(
|
92
|
+
# from: "sender@example.com",
|
93
|
+
# to: "recipient@example.com",
|
94
|
+
# subject: "Hello",
|
95
|
+
# html: "<h1>Hello!</h1>"
|
96
|
+
# )
|
97
|
+
# response = client.send_email(email)
|
98
|
+
#
|
99
|
+
# @example With hash
|
100
|
+
# response = client.send_email({
|
101
|
+
# from: "sender@example.com",
|
102
|
+
# to: "recipient@example.com",
|
103
|
+
# subject: "Hello",
|
104
|
+
# text: "Hello!"
|
105
|
+
# })
|
106
|
+
def send_email(email)
|
107
|
+
email_obj = email.is_a?(Email) ? email : create_email_from_hash(email)
|
108
|
+
|
109
|
+
response_data = @http_client.post("v1/send-email", email_obj.to_h)
|
110
|
+
EmailResponse.from_api_response(response_data)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Send an email with individual parameters
|
114
|
+
#
|
115
|
+
# @param from [String] sender email address
|
116
|
+
# @param to [String] recipient email address
|
117
|
+
# @param subject [String] email subject
|
118
|
+
# @param html [String, nil] HTML content
|
119
|
+
# @param text [String, nil] plain text content
|
120
|
+
# @return [EmailResponse] the response from the API
|
121
|
+
# @raise [ValidationError] if email data is invalid
|
122
|
+
# @raise [Poodle::Error] if the request fails
|
123
|
+
#
|
124
|
+
# @example
|
125
|
+
# response = client.send(
|
126
|
+
# from: "sender@example.com",
|
127
|
+
# to: "recipient@example.com",
|
128
|
+
# subject: "Hello World",
|
129
|
+
# html: "<h1>Hello!</h1>",
|
130
|
+
# text: "Hello!"
|
131
|
+
# )
|
132
|
+
def send(from:, to:, subject:, html: nil, text: nil)
|
133
|
+
email = Email.new(from: from, to: to, subject: subject, html: html, text: text)
|
134
|
+
send_email(email)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Send an HTML email
|
138
|
+
#
|
139
|
+
# @param from [String] sender email address
|
140
|
+
# @param to [String] recipient email address
|
141
|
+
# @param subject [String] email subject
|
142
|
+
# @param html [String] HTML content
|
143
|
+
# @return [EmailResponse] the response from the API
|
144
|
+
# @raise [ValidationError] if email data is invalid
|
145
|
+
# @raise [Poodle::Error] if the request fails
|
146
|
+
#
|
147
|
+
# @example
|
148
|
+
# response = client.send_html(
|
149
|
+
# from: "sender@example.com",
|
150
|
+
# to: "recipient@example.com",
|
151
|
+
# subject: "Newsletter",
|
152
|
+
# html: "<h1>Newsletter</h1><p>Content here</p>"
|
153
|
+
# )
|
154
|
+
def send_html(from:, to:, subject:, html:)
|
155
|
+
send(from: from, to: to, subject: subject, html: html)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Send a plain text email
|
159
|
+
#
|
160
|
+
# @param from [String] sender email address
|
161
|
+
# @param to [String] recipient email address
|
162
|
+
# @param subject [String] email subject
|
163
|
+
# @param text [String] plain text content
|
164
|
+
# @return [EmailResponse] the response from the API
|
165
|
+
# @raise [ValidationError] if email data is invalid
|
166
|
+
# @raise [Poodle::Error] if the request fails
|
167
|
+
#
|
168
|
+
# @example
|
169
|
+
# response = client.send_text(
|
170
|
+
# from: "sender@example.com",
|
171
|
+
# to: "recipient@example.com",
|
172
|
+
# subject: "Simple notification",
|
173
|
+
# text: "This is a simple text notification."
|
174
|
+
# )
|
175
|
+
def send_text(from:, to:, subject:, text:)
|
176
|
+
send(from: from, to: to, subject: subject, text: text)
|
177
|
+
end
|
178
|
+
|
179
|
+
# Get the SDK version
|
180
|
+
#
|
181
|
+
# @return [String] the SDK version
|
182
|
+
def version
|
183
|
+
@config.class::SDK_VERSION
|
184
|
+
end
|
185
|
+
|
186
|
+
private
|
187
|
+
|
188
|
+
# Create an Email object from hash data
|
189
|
+
#
|
190
|
+
# @param data [Hash] email data
|
191
|
+
# @return [Email] the email object
|
192
|
+
# @raise [ValidationError] if required fields are missing
|
193
|
+
def create_email_from_hash(data)
|
194
|
+
validate_required_email_fields(data)
|
195
|
+
extract_email_from_hash(data)
|
196
|
+
end
|
197
|
+
|
198
|
+
# Validate required email fields
|
199
|
+
#
|
200
|
+
# @param data [Hash] email data
|
201
|
+
# @raise [ValidationError] if required fields are missing
|
202
|
+
def validate_required_email_fields(data)
|
203
|
+
raise ValidationError.missing_field("from") unless data[:from] || data["from"]
|
204
|
+
raise ValidationError.missing_field("to") unless data[:to] || data["to"]
|
205
|
+
raise ValidationError.missing_field("subject") unless data[:subject] || data["subject"]
|
206
|
+
end
|
207
|
+
|
208
|
+
# Extract email object from hash data
|
209
|
+
#
|
210
|
+
# @param data [Hash] email data
|
211
|
+
# @return [Email] the email object
|
212
|
+
def extract_email_from_hash(data)
|
213
|
+
Email.new(
|
214
|
+
from: data[:from] || data["from"],
|
215
|
+
to: data[:to] || data["to"],
|
216
|
+
subject: data[:subject] || data["subject"],
|
217
|
+
html: data[:html] || data["html"],
|
218
|
+
text: data[:text] || data["text"]
|
219
|
+
)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "uri"
|
4
|
+
|
5
|
+
module Poodle
|
6
|
+
# Configuration class for Poodle SDK settings
|
7
|
+
#
|
8
|
+
# @example Basic configuration
|
9
|
+
# config = Poodle::Configuration.new(api_key: "your_api_key")
|
10
|
+
#
|
11
|
+
# @example Using environment variables
|
12
|
+
# ENV["POODLE_API_KEY"] = "your_api_key"
|
13
|
+
# config = Poodle::Configuration.new
|
14
|
+
#
|
15
|
+
# @example Full configuration
|
16
|
+
# config = Poodle::Configuration.new(
|
17
|
+
# api_key: "your_api_key",
|
18
|
+
# base_url: "https://api.usepoodle.com",
|
19
|
+
# timeout: 30,
|
20
|
+
# connect_timeout: 10,
|
21
|
+
# debug: true
|
22
|
+
# )
|
23
|
+
class Configuration
|
24
|
+
# Default API base URL
|
25
|
+
DEFAULT_BASE_URL = "https://api.usepoodle.com"
|
26
|
+
|
27
|
+
# Default timeout in seconds
|
28
|
+
DEFAULT_TIMEOUT = 30
|
29
|
+
|
30
|
+
# Default connect timeout in seconds
|
31
|
+
DEFAULT_CONNECT_TIMEOUT = 10
|
32
|
+
|
33
|
+
# Maximum content size in bytes (10MB)
|
34
|
+
MAX_CONTENT_SIZE = 10 * 1024 * 1024
|
35
|
+
|
36
|
+
# SDK version
|
37
|
+
SDK_VERSION = Poodle::VERSION
|
38
|
+
|
39
|
+
# @return [String] the API key for authentication
|
40
|
+
attr_reader :api_key
|
41
|
+
|
42
|
+
# @return [String] the base URL for the API
|
43
|
+
attr_reader :base_url
|
44
|
+
|
45
|
+
# @return [Integer] the request timeout in seconds
|
46
|
+
attr_reader :timeout
|
47
|
+
|
48
|
+
# @return [Integer] the connection timeout in seconds
|
49
|
+
attr_reader :connect_timeout
|
50
|
+
|
51
|
+
# @return [Boolean] whether debug mode is enabled
|
52
|
+
attr_reader :debug
|
53
|
+
|
54
|
+
# @return [Hash] additional HTTP client options
|
55
|
+
attr_reader :http_options
|
56
|
+
|
57
|
+
# Initialize a new Configuration
|
58
|
+
#
|
59
|
+
# @param api_key [String, nil] API key (defaults to POODLE_API_KEY env var)
|
60
|
+
# @param base_url [String, nil] Base URL (defaults to POODLE_BASE_URL env var or DEFAULT_BASE_URL)
|
61
|
+
# @param timeout [Integer, nil] Request timeout (defaults to POODLE_TIMEOUT env var or DEFAULT_TIMEOUT)
|
62
|
+
# @param connect_timeout [Integer, nil] Connect timeout (defaults to POODLE_CONNECT_TIMEOUT env var or
|
63
|
+
# DEFAULT_CONNECT_TIMEOUT)
|
64
|
+
# @param debug [Boolean] Enable debug mode (defaults to POODLE_DEBUG env var or false)
|
65
|
+
# @param http_options [Hash] Additional HTTP client options
|
66
|
+
#
|
67
|
+
# @raise [ArgumentError] if api_key is missing or invalid
|
68
|
+
# @raise [ArgumentError] if base_url is invalid
|
69
|
+
# @raise [ArgumentError] if timeout values are invalid
|
70
|
+
def initialize(**options)
|
71
|
+
api_key = options[:api_key]
|
72
|
+
base_url = options[:base_url]
|
73
|
+
timeout = options[:timeout]
|
74
|
+
connect_timeout = options[:connect_timeout]
|
75
|
+
debug = options.fetch(:debug, false)
|
76
|
+
http_options = options.fetch(:http_options, {})
|
77
|
+
@api_key = api_key || ENV.fetch("POODLE_API_KEY", nil)
|
78
|
+
@base_url = base_url || ENV["POODLE_BASE_URL"] || DEFAULT_BASE_URL
|
79
|
+
@timeout = timeout || ENV.fetch("POODLE_TIMEOUT", DEFAULT_TIMEOUT).to_i
|
80
|
+
@connect_timeout = connect_timeout || ENV.fetch("POODLE_CONNECT_TIMEOUT", DEFAULT_CONNECT_TIMEOUT).to_i
|
81
|
+
@debug = debug || ENV["POODLE_DEBUG"] == "true"
|
82
|
+
@http_options = http_options
|
83
|
+
|
84
|
+
validate!
|
85
|
+
end
|
86
|
+
|
87
|
+
# Get the User-Agent string for HTTP requests
|
88
|
+
#
|
89
|
+
# @return [String] the User-Agent string
|
90
|
+
def user_agent
|
91
|
+
"poodle-ruby/#{SDK_VERSION} (Ruby #{RUBY_VERSION})"
|
92
|
+
end
|
93
|
+
|
94
|
+
# Get the full URL for an endpoint
|
95
|
+
#
|
96
|
+
# @param endpoint [String] the API endpoint
|
97
|
+
# @return [String] the full URL
|
98
|
+
def url_for(endpoint)
|
99
|
+
"#{@base_url}/#{endpoint.gsub(%r{^/}, '')}"
|
100
|
+
end
|
101
|
+
|
102
|
+
# Check if debug mode is enabled
|
103
|
+
#
|
104
|
+
# @return [Boolean] true if debug mode is enabled
|
105
|
+
def debug?
|
106
|
+
@debug
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
# Validate the configuration
|
112
|
+
#
|
113
|
+
# @raise [ArgumentError] if any configuration is invalid
|
114
|
+
def validate!
|
115
|
+
raise ArgumentError, "API key is required" if @api_key.nil? || @api_key.empty?
|
116
|
+
|
117
|
+
validate_url!(@base_url, "base_url")
|
118
|
+
validate_timeout!(@timeout, "timeout")
|
119
|
+
validate_timeout!(@connect_timeout, "connect_timeout")
|
120
|
+
end
|
121
|
+
|
122
|
+
# Validate a URL
|
123
|
+
#
|
124
|
+
# @param url [String] the URL to validate
|
125
|
+
# @param field [String] the field name for error messages
|
126
|
+
# @raise [ArgumentError] if the URL is invalid
|
127
|
+
def validate_url!(url, field)
|
128
|
+
return if url.nil? || url.empty?
|
129
|
+
|
130
|
+
uri = URI.parse(url)
|
131
|
+
raise ArgumentError, "#{field} must be a valid HTTP or HTTPS URL" unless %w[http https].include?(uri.scheme)
|
132
|
+
rescue URI::InvalidURIError
|
133
|
+
raise ArgumentError, "#{field} must be a valid URL"
|
134
|
+
end
|
135
|
+
|
136
|
+
# Validate a timeout value
|
137
|
+
#
|
138
|
+
# @param value [Integer] the timeout value
|
139
|
+
# @param field [String] the field name for error messages
|
140
|
+
# @raise [ArgumentError] if the timeout is invalid
|
141
|
+
def validate_timeout!(value, field)
|
142
|
+
raise ArgumentError, "#{field} must be a positive integer" unless value.is_a?(Integer) && value.positive?
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
data/lib/poodle/email.rb
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "uri"
|
4
|
+
require_relative "configuration"
|
5
|
+
require_relative "errors/validation_error"
|
6
|
+
|
7
|
+
module Poodle
|
8
|
+
# Email model representing an email to be sent
|
9
|
+
#
|
10
|
+
# @example Creating an email
|
11
|
+
# email = Poodle::Email.new(
|
12
|
+
# from: "sender@example.com",
|
13
|
+
# to: "recipient@example.com",
|
14
|
+
# subject: "Hello World",
|
15
|
+
# html: "<h1>Hello!</h1>",
|
16
|
+
# text: "Hello!"
|
17
|
+
# )
|
18
|
+
#
|
19
|
+
# @example HTML only email
|
20
|
+
# email = Poodle::Email.new(
|
21
|
+
# from: "sender@example.com",
|
22
|
+
# to: "recipient@example.com",
|
23
|
+
# subject: "Newsletter",
|
24
|
+
# html: "<h1>Newsletter</h1><p>Content here</p>"
|
25
|
+
# )
|
26
|
+
#
|
27
|
+
# @example Text only email
|
28
|
+
# email = Poodle::Email.new(
|
29
|
+
# from: "sender@example.com",
|
30
|
+
# to: "recipient@example.com",
|
31
|
+
# subject: "Simple notification",
|
32
|
+
# text: "This is a simple text notification."
|
33
|
+
# )
|
34
|
+
class Email
|
35
|
+
# @return [String] sender email address
|
36
|
+
attr_reader :from
|
37
|
+
|
38
|
+
# @return [String] recipient email address
|
39
|
+
attr_reader :to
|
40
|
+
|
41
|
+
# @return [String] email subject
|
42
|
+
attr_reader :subject
|
43
|
+
|
44
|
+
# @return [String, nil] HTML content
|
45
|
+
attr_reader :html
|
46
|
+
|
47
|
+
# @return [String, nil] plain text content
|
48
|
+
attr_reader :text
|
49
|
+
|
50
|
+
# Initialize a new Email
|
51
|
+
#
|
52
|
+
# @param from [String] sender email address
|
53
|
+
# @param to [String] recipient email address
|
54
|
+
# @param subject [String] email subject
|
55
|
+
# @param html [String, nil] HTML content
|
56
|
+
# @param text [String, nil] plain text content
|
57
|
+
#
|
58
|
+
# @raise [ValidationError] if any field is invalid
|
59
|
+
def initialize(from:, to:, subject:, html: nil, text: nil)
|
60
|
+
@from = from
|
61
|
+
@to = to
|
62
|
+
@subject = subject
|
63
|
+
@html = html
|
64
|
+
@text = text
|
65
|
+
|
66
|
+
validate!
|
67
|
+
freeze # Make the object immutable
|
68
|
+
end
|
69
|
+
|
70
|
+
# Convert email to hash for API request
|
71
|
+
#
|
72
|
+
# @return [Hash] email data as hash
|
73
|
+
def to_h
|
74
|
+
data = {
|
75
|
+
from: @from,
|
76
|
+
to: @to,
|
77
|
+
subject: @subject
|
78
|
+
}
|
79
|
+
|
80
|
+
data[:html] = @html if @html
|
81
|
+
data[:text] = @text if @text
|
82
|
+
|
83
|
+
data
|
84
|
+
end
|
85
|
+
|
86
|
+
# Convert email to JSON string
|
87
|
+
#
|
88
|
+
# @return [String] email data as JSON
|
89
|
+
def to_json(*args)
|
90
|
+
require "json"
|
91
|
+
to_h.to_json(*args)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Check if email has HTML content
|
95
|
+
#
|
96
|
+
# @return [Boolean] true if HTML content is present
|
97
|
+
def html?
|
98
|
+
!@html.nil? && !@html.empty?
|
99
|
+
end
|
100
|
+
|
101
|
+
# Check if email has text content
|
102
|
+
#
|
103
|
+
# @return [Boolean] true if text content is present
|
104
|
+
def text?
|
105
|
+
!@text.nil? && !@text.empty?
|
106
|
+
end
|
107
|
+
|
108
|
+
# Check if email is multipart (has both HTML and text)
|
109
|
+
#
|
110
|
+
# @return [Boolean] true if both HTML and text content are present
|
111
|
+
def multipart?
|
112
|
+
html? && text?
|
113
|
+
end
|
114
|
+
|
115
|
+
# Get the size of the email content in bytes
|
116
|
+
#
|
117
|
+
# @return [Integer] total size of HTML and text content
|
118
|
+
def content_size
|
119
|
+
size = 0
|
120
|
+
size += @html.bytesize if @html
|
121
|
+
size += @text.bytesize if @text
|
122
|
+
size
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
# Validate email data
|
128
|
+
#
|
129
|
+
# @raise [ValidationError] if any validation fails
|
130
|
+
def validate!
|
131
|
+
validate_required_fields!
|
132
|
+
validate_email_addresses!
|
133
|
+
validate_content!
|
134
|
+
validate_content_size!
|
135
|
+
end
|
136
|
+
|
137
|
+
# Validate required fields
|
138
|
+
#
|
139
|
+
# @raise [ValidationError] if any required field is missing
|
140
|
+
def validate_required_fields!
|
141
|
+
raise ValidationError.missing_field("from") if @from.nil? || @from.empty?
|
142
|
+
raise ValidationError.missing_field("to") if @to.nil? || @to.empty?
|
143
|
+
raise ValidationError.missing_field("subject") if @subject.nil? || @subject.empty?
|
144
|
+
end
|
145
|
+
|
146
|
+
# Validate email addresses
|
147
|
+
#
|
148
|
+
# @raise [ValidationError] if any email address is invalid
|
149
|
+
def validate_email_addresses!
|
150
|
+
raise ValidationError.invalid_email(@from, field: "from") unless valid_email?(@from)
|
151
|
+
raise ValidationError.invalid_email(@to, field: "to") unless valid_email?(@to)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Validate content presence
|
155
|
+
#
|
156
|
+
# @raise [ValidationError] if no content is provided
|
157
|
+
def validate_content!
|
158
|
+
return if html? || text?
|
159
|
+
|
160
|
+
raise ValidationError.invalid_content
|
161
|
+
end
|
162
|
+
|
163
|
+
# Validate content size
|
164
|
+
#
|
165
|
+
# @raise [ValidationError] if content is too large
|
166
|
+
def validate_content_size!
|
167
|
+
max_size = Configuration::MAX_CONTENT_SIZE
|
168
|
+
|
169
|
+
raise ValidationError.content_too_large("html", max_size) if @html && @html.bytesize > max_size
|
170
|
+
|
171
|
+
return unless @text && @text.bytesize > max_size
|
172
|
+
|
173
|
+
raise ValidationError.content_too_large("text", max_size)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Check if an email address is valid
|
177
|
+
#
|
178
|
+
# @param email [String] the email address to validate
|
179
|
+
# @return [Boolean] true if the email is valid
|
180
|
+
def valid_email?(email)
|
181
|
+
return false if email.nil? || email.empty?
|
182
|
+
|
183
|
+
# Basic email validation using URI::MailTo
|
184
|
+
uri = URI::MailTo.build([email, nil])
|
185
|
+
uri.to_s == "mailto:#{email}"
|
186
|
+
rescue URI::InvalidComponentError, ArgumentError
|
187
|
+
false
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|