postmark_ruby_client 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 +7 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +28 -0
- data/LICENSE.txt +21 -0
- data/README.md +278 -0
- data/Rakefile +24 -0
- data/lib/postmark_client/client/base.rb +163 -0
- data/lib/postmark_client/configuration.rb +79 -0
- data/lib/postmark_client/errors.rb +52 -0
- data/lib/postmark_client/models/attachment.rb +126 -0
- data/lib/postmark_client/models/email.rb +254 -0
- data/lib/postmark_client/models/email_response.rb +88 -0
- data/lib/postmark_client/resources/emails.rb +159 -0
- data/lib/postmark_client/version.rb +7 -0
- data/lib/postmark_client.rb +78 -0
- metadata +191 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
|
|
5
|
+
module PostmarkClient
|
|
6
|
+
# Represents an email attachment for the Postmark API.
|
|
7
|
+
#
|
|
8
|
+
# @example Creating a simple attachment
|
|
9
|
+
# attachment = PostmarkClient::Attachment.new(
|
|
10
|
+
# name: "document.pdf",
|
|
11
|
+
# content: File.read("document.pdf"),
|
|
12
|
+
# content_type: "application/pdf"
|
|
13
|
+
# )
|
|
14
|
+
#
|
|
15
|
+
# @example Creating an inline image attachment
|
|
16
|
+
# attachment = PostmarkClient::Attachment.new(
|
|
17
|
+
# name: "logo.png",
|
|
18
|
+
# content: File.read("logo.png"),
|
|
19
|
+
# content_type: "image/png",
|
|
20
|
+
# content_id: "cid:logo.png"
|
|
21
|
+
# )
|
|
22
|
+
#
|
|
23
|
+
# @example Creating from a file path
|
|
24
|
+
# attachment = PostmarkClient::Attachment.from_file("path/to/file.pdf")
|
|
25
|
+
class Attachment
|
|
26
|
+
# @return [String] the filename of the attachment
|
|
27
|
+
attr_accessor :name
|
|
28
|
+
|
|
29
|
+
# @return [String] the Base64-encoded content of the attachment
|
|
30
|
+
attr_accessor :content
|
|
31
|
+
|
|
32
|
+
# @return [String] the MIME type of the attachment
|
|
33
|
+
attr_accessor :content_type
|
|
34
|
+
|
|
35
|
+
# @return [String, nil] the content ID for inline attachments
|
|
36
|
+
attr_accessor :content_id
|
|
37
|
+
|
|
38
|
+
# Initialize a new attachment
|
|
39
|
+
#
|
|
40
|
+
# @param name [String] the filename
|
|
41
|
+
# @param content [String] the file content (will be Base64 encoded if not already)
|
|
42
|
+
# @param content_type [String] the MIME type
|
|
43
|
+
# @param content_id [String, nil] optional content ID for inline images
|
|
44
|
+
# @param base64_encoded [Boolean] whether content is already Base64 encoded
|
|
45
|
+
def initialize(name:, content:, content_type:, content_id: nil, base64_encoded: false)
|
|
46
|
+
@name = name
|
|
47
|
+
@content = base64_encoded ? content : Base64.strict_encode64(content)
|
|
48
|
+
@content_type = content_type
|
|
49
|
+
@content_id = content_id
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Create an attachment from a file path
|
|
53
|
+
#
|
|
54
|
+
# @param file_path [String] path to the file
|
|
55
|
+
# @param content_type [String, nil] optional MIME type (auto-detected if not provided)
|
|
56
|
+
# @param content_id [String, nil] optional content ID for inline images
|
|
57
|
+
# @return [Attachment] a new attachment instance
|
|
58
|
+
def self.from_file(file_path, content_type: nil, content_id: nil)
|
|
59
|
+
name = File.basename(file_path)
|
|
60
|
+
content = File.binread(file_path)
|
|
61
|
+
detected_content_type = content_type || detect_content_type(name)
|
|
62
|
+
|
|
63
|
+
new(
|
|
64
|
+
name: name,
|
|
65
|
+
content: content,
|
|
66
|
+
content_type: detected_content_type,
|
|
67
|
+
content_id: content_id
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Convert the attachment to a hash for API requests
|
|
72
|
+
#
|
|
73
|
+
# @return [Hash] the attachment as a hash
|
|
74
|
+
def to_h
|
|
75
|
+
hash = {
|
|
76
|
+
"Name" => name,
|
|
77
|
+
"Content" => content,
|
|
78
|
+
"ContentType" => content_type
|
|
79
|
+
}
|
|
80
|
+
hash["ContentID"] = content_id if content_id
|
|
81
|
+
hash
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Check if this is an inline attachment
|
|
85
|
+
#
|
|
86
|
+
# @return [Boolean] true if this is an inline attachment
|
|
87
|
+
def inline?
|
|
88
|
+
!content_id.nil?
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
# Detect content type from file extension
|
|
94
|
+
#
|
|
95
|
+
# @param filename [String] the filename
|
|
96
|
+
# @return [String] the detected MIME type
|
|
97
|
+
def self.detect_content_type(filename)
|
|
98
|
+
extension = File.extname(filename).downcase.delete(".")
|
|
99
|
+
|
|
100
|
+
CONTENT_TYPES.fetch(extension, "application/octet-stream")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Common content types by file extension
|
|
104
|
+
CONTENT_TYPES = {
|
|
105
|
+
"pdf" => "application/pdf",
|
|
106
|
+
"doc" => "application/msword",
|
|
107
|
+
"docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
108
|
+
"xls" => "application/vnd.ms-excel",
|
|
109
|
+
"xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
110
|
+
"png" => "image/png",
|
|
111
|
+
"jpg" => "image/jpeg",
|
|
112
|
+
"jpeg" => "image/jpeg",
|
|
113
|
+
"gif" => "image/gif",
|
|
114
|
+
"svg" => "image/svg+xml",
|
|
115
|
+
"txt" => "text/plain",
|
|
116
|
+
"html" => "text/html",
|
|
117
|
+
"htm" => "text/html",
|
|
118
|
+
"css" => "text/css",
|
|
119
|
+
"js" => "application/javascript",
|
|
120
|
+
"json" => "application/json",
|
|
121
|
+
"xml" => "application/xml",
|
|
122
|
+
"zip" => "application/zip",
|
|
123
|
+
"csv" => "text/csv"
|
|
124
|
+
}.freeze
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PostmarkClient
|
|
4
|
+
# Represents an email message for the Postmark API.
|
|
5
|
+
# Provides a clean Ruby interface for building email payloads.
|
|
6
|
+
#
|
|
7
|
+
# @example Creating a simple email
|
|
8
|
+
# email = PostmarkClient::Email.new(
|
|
9
|
+
# from: "sender@example.com",
|
|
10
|
+
# to: "recipient@example.com",
|
|
11
|
+
# subject: "Hello!",
|
|
12
|
+
# text_body: "Hello, World!"
|
|
13
|
+
# )
|
|
14
|
+
#
|
|
15
|
+
# @example Creating an email with HTML and attachments
|
|
16
|
+
# email = PostmarkClient::Email.new(
|
|
17
|
+
# from: "John Doe <john@example.com>",
|
|
18
|
+
# to: ["alice@example.com", "bob@example.com"],
|
|
19
|
+
# cc: "manager@example.com",
|
|
20
|
+
# subject: "Monthly Report",
|
|
21
|
+
# html_body: "<h1>Report</h1><p>See attached.</p>",
|
|
22
|
+
# text_body: "Report - See attached.",
|
|
23
|
+
# track_opens: true
|
|
24
|
+
# )
|
|
25
|
+
# email.add_attachment(name: "report.pdf", content: pdf_data, content_type: "application/pdf")
|
|
26
|
+
#
|
|
27
|
+
# @example Using the builder pattern
|
|
28
|
+
# email = PostmarkClient::Email.new
|
|
29
|
+
# .from("sender@example.com")
|
|
30
|
+
# .to("recipient@example.com")
|
|
31
|
+
# .subject("Hello")
|
|
32
|
+
# .text_body("Hello, World!")
|
|
33
|
+
class Email
|
|
34
|
+
# @return [String] sender email address
|
|
35
|
+
attr_accessor :from
|
|
36
|
+
|
|
37
|
+
# @return [String, Array<String>] recipient email address(es)
|
|
38
|
+
attr_accessor :to
|
|
39
|
+
|
|
40
|
+
# @return [String, Array<String>, nil] CC recipient email address(es)
|
|
41
|
+
attr_accessor :cc
|
|
42
|
+
|
|
43
|
+
# @return [String, Array<String>, nil] BCC recipient email address(es)
|
|
44
|
+
attr_accessor :bcc
|
|
45
|
+
|
|
46
|
+
# @return [String] email subject
|
|
47
|
+
attr_accessor :subject
|
|
48
|
+
|
|
49
|
+
# @return [String, nil] HTML email body
|
|
50
|
+
attr_accessor :html_body
|
|
51
|
+
|
|
52
|
+
# @return [String, nil] plain text email body
|
|
53
|
+
attr_accessor :text_body
|
|
54
|
+
|
|
55
|
+
# @return [String, nil] reply-to email address
|
|
56
|
+
attr_accessor :reply_to
|
|
57
|
+
|
|
58
|
+
# @return [String, nil] email tag for categorization
|
|
59
|
+
attr_accessor :tag
|
|
60
|
+
|
|
61
|
+
# @return [Array<Hash>] custom email headers
|
|
62
|
+
attr_accessor :headers
|
|
63
|
+
|
|
64
|
+
# @return [Boolean] whether to track email opens
|
|
65
|
+
attr_accessor :track_opens
|
|
66
|
+
|
|
67
|
+
# @return [String] link tracking setting ("None", "HtmlAndText", "HtmlOnly", "TextOnly")
|
|
68
|
+
attr_accessor :track_links
|
|
69
|
+
|
|
70
|
+
# @return [Array<Attachment>] email attachments
|
|
71
|
+
attr_accessor :attachments
|
|
72
|
+
|
|
73
|
+
# @return [Hash] custom metadata key-value pairs
|
|
74
|
+
attr_accessor :metadata
|
|
75
|
+
|
|
76
|
+
# @return [String] message stream identifier
|
|
77
|
+
attr_accessor :message_stream
|
|
78
|
+
|
|
79
|
+
# Valid link tracking options
|
|
80
|
+
TRACK_LINKS_OPTIONS = %w[None HtmlAndText HtmlOnly TextOnly].freeze
|
|
81
|
+
|
|
82
|
+
# Initialize a new email
|
|
83
|
+
#
|
|
84
|
+
# @param from [String, nil] sender email address
|
|
85
|
+
# @param to [String, Array<String>, nil] recipient email address(es)
|
|
86
|
+
# @param cc [String, Array<String>, nil] CC recipient(s)
|
|
87
|
+
# @param bcc [String, Array<String>, nil] BCC recipient(s)
|
|
88
|
+
# @param subject [String, nil] email subject
|
|
89
|
+
# @param html_body [String, nil] HTML body
|
|
90
|
+
# @param text_body [String, nil] plain text body
|
|
91
|
+
# @param reply_to [String, nil] reply-to address
|
|
92
|
+
# @param tag [String, nil] email tag
|
|
93
|
+
# @param headers [Array<Hash>, nil] custom headers
|
|
94
|
+
# @param track_opens [Boolean, nil] track opens
|
|
95
|
+
# @param track_links [String, nil] link tracking setting
|
|
96
|
+
# @param attachments [Array<Attachment>, nil] attachments
|
|
97
|
+
# @param metadata [Hash, nil] custom metadata
|
|
98
|
+
# @param message_stream [String, nil] message stream
|
|
99
|
+
def initialize(
|
|
100
|
+
from: nil,
|
|
101
|
+
to: nil,
|
|
102
|
+
cc: nil,
|
|
103
|
+
bcc: nil,
|
|
104
|
+
subject: nil,
|
|
105
|
+
html_body: nil,
|
|
106
|
+
text_body: nil,
|
|
107
|
+
reply_to: nil,
|
|
108
|
+
tag: nil,
|
|
109
|
+
headers: nil,
|
|
110
|
+
track_opens: nil,
|
|
111
|
+
track_links: nil,
|
|
112
|
+
attachments: nil,
|
|
113
|
+
metadata: nil,
|
|
114
|
+
message_stream: nil
|
|
115
|
+
)
|
|
116
|
+
@from = from
|
|
117
|
+
@to = to
|
|
118
|
+
@cc = cc
|
|
119
|
+
@bcc = bcc
|
|
120
|
+
@subject = subject
|
|
121
|
+
@html_body = html_body
|
|
122
|
+
@text_body = text_body
|
|
123
|
+
@reply_to = reply_to
|
|
124
|
+
@tag = tag
|
|
125
|
+
@headers = headers || []
|
|
126
|
+
@track_opens = track_opens
|
|
127
|
+
@track_links = track_links
|
|
128
|
+
@attachments = attachments || []
|
|
129
|
+
@metadata = metadata || {}
|
|
130
|
+
@message_stream = message_stream || PostmarkClient.configuration.default_message_stream
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Add a custom header to the email
|
|
134
|
+
#
|
|
135
|
+
# @param name [String] header name
|
|
136
|
+
# @param value [String] header value
|
|
137
|
+
# @return [self] returns self for method chaining
|
|
138
|
+
def add_header(name:, value:)
|
|
139
|
+
@headers << { "Name" => name, "Value" => value }
|
|
140
|
+
self
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Add an attachment to the email
|
|
144
|
+
#
|
|
145
|
+
# @param attachment [Attachment] an Attachment instance
|
|
146
|
+
# @return [self] returns self for method chaining
|
|
147
|
+
#
|
|
148
|
+
# @overload add_attachment(attachment)
|
|
149
|
+
# @param attachment [Attachment] an Attachment instance
|
|
150
|
+
#
|
|
151
|
+
# @overload add_attachment(name:, content:, content_type:, content_id: nil)
|
|
152
|
+
# @param name [String] filename
|
|
153
|
+
# @param content [String] file content
|
|
154
|
+
# @param content_type [String] MIME type
|
|
155
|
+
# @param content_id [String, nil] content ID for inline attachments
|
|
156
|
+
def add_attachment(attachment = nil, **kwargs)
|
|
157
|
+
if attachment.is_a?(Attachment)
|
|
158
|
+
@attachments << attachment
|
|
159
|
+
elsif kwargs.any?
|
|
160
|
+
@attachments << Attachment.new(**kwargs)
|
|
161
|
+
else
|
|
162
|
+
raise ArgumentError, "Must provide an Attachment instance or attachment parameters"
|
|
163
|
+
end
|
|
164
|
+
self
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Add a file as an attachment
|
|
168
|
+
#
|
|
169
|
+
# @param file_path [String] path to the file
|
|
170
|
+
# @param content_type [String, nil] optional MIME type
|
|
171
|
+
# @param content_id [String, nil] optional content ID
|
|
172
|
+
# @return [self] returns self for method chaining
|
|
173
|
+
def attach_file(file_path, content_type: nil, content_id: nil)
|
|
174
|
+
@attachments << Attachment.from_file(file_path, content_type: content_type, content_id: content_id)
|
|
175
|
+
self
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Add metadata to the email
|
|
179
|
+
#
|
|
180
|
+
# @param key [String, Symbol] metadata key
|
|
181
|
+
# @param value [String] metadata value
|
|
182
|
+
# @return [self] returns self for method chaining
|
|
183
|
+
def add_metadata(key, value)
|
|
184
|
+
@metadata[key.to_s] = value
|
|
185
|
+
self
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Validate the email before sending
|
|
189
|
+
#
|
|
190
|
+
# @return [Boolean] true if valid
|
|
191
|
+
# @raise [ValidationError] if validation fails
|
|
192
|
+
def validate!
|
|
193
|
+
raise ValidationError, "From address is required" if from.nil? || from.empty?
|
|
194
|
+
raise ValidationError, "To address is required" if to.nil? || (to.is_a?(Array) && to.empty?) || (to.is_a?(String) && to.empty?)
|
|
195
|
+
raise ValidationError, "Either HtmlBody or TextBody is required" if (html_body.nil? || html_body.empty?) && (text_body.nil? || text_body.empty?)
|
|
196
|
+
|
|
197
|
+
if track_links && !TRACK_LINKS_OPTIONS.include?(track_links)
|
|
198
|
+
raise ValidationError, "TrackLinks must be one of: #{TRACK_LINKS_OPTIONS.join(', ')}"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
true
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Check if the email is valid
|
|
205
|
+
#
|
|
206
|
+
# @return [Boolean] true if valid, false otherwise
|
|
207
|
+
def valid?
|
|
208
|
+
validate!
|
|
209
|
+
true
|
|
210
|
+
rescue ValidationError
|
|
211
|
+
false
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Convert the email to a hash for API requests
|
|
215
|
+
#
|
|
216
|
+
# @return [Hash] the email as a hash matching Postmark API format
|
|
217
|
+
def to_h
|
|
218
|
+
hash = {}
|
|
219
|
+
|
|
220
|
+
hash["From"] = from if from
|
|
221
|
+
hash["To"] = normalize_recipients(to) if to
|
|
222
|
+
hash["Cc"] = normalize_recipients(cc) if cc
|
|
223
|
+
hash["Bcc"] = normalize_recipients(bcc) if bcc
|
|
224
|
+
hash["Subject"] = subject if subject
|
|
225
|
+
hash["HtmlBody"] = html_body if html_body
|
|
226
|
+
hash["TextBody"] = text_body if text_body
|
|
227
|
+
hash["ReplyTo"] = reply_to if reply_to
|
|
228
|
+
hash["Tag"] = tag if tag
|
|
229
|
+
hash["Headers"] = headers if headers.any?
|
|
230
|
+
hash["TrackOpens"] = track_opens unless track_opens.nil?
|
|
231
|
+
hash["TrackLinks"] = track_links if track_links
|
|
232
|
+
hash["Attachments"] = attachments.map(&:to_h) if attachments.any?
|
|
233
|
+
hash["Metadata"] = metadata if metadata.any?
|
|
234
|
+
hash["MessageStream"] = message_stream if message_stream
|
|
235
|
+
|
|
236
|
+
hash
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Alias for to_h
|
|
240
|
+
alias_method :to_api_hash, :to_h
|
|
241
|
+
|
|
242
|
+
private
|
|
243
|
+
|
|
244
|
+
# Normalize recipients to comma-separated string
|
|
245
|
+
#
|
|
246
|
+
# @param recipients [String, Array<String>] recipient(s)
|
|
247
|
+
# @return [String] comma-separated recipients
|
|
248
|
+
def normalize_recipients(recipients)
|
|
249
|
+
return recipients if recipients.is_a?(String)
|
|
250
|
+
|
|
251
|
+
recipients.join(", ")
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PostmarkClient
|
|
4
|
+
# Represents a response from the Postmark Email API.
|
|
5
|
+
# Wraps the API response in a convenient Ruby object.
|
|
6
|
+
#
|
|
7
|
+
# @example Handling a successful response
|
|
8
|
+
# response = client.send_email(email)
|
|
9
|
+
# puts "Message ID: #{response.message_id}"
|
|
10
|
+
# puts "Submitted at: #{response.submitted_at}"
|
|
11
|
+
#
|
|
12
|
+
# @example Checking for errors
|
|
13
|
+
# if response.success?
|
|
14
|
+
# puts "Email sent successfully!"
|
|
15
|
+
# else
|
|
16
|
+
# puts "Error: #{response.message}"
|
|
17
|
+
# end
|
|
18
|
+
class EmailResponse
|
|
19
|
+
# @return [String] recipient email address
|
|
20
|
+
attr_reader :to
|
|
21
|
+
|
|
22
|
+
# @return [Time] timestamp when the email was submitted
|
|
23
|
+
attr_reader :submitted_at
|
|
24
|
+
|
|
25
|
+
# @return [String] unique message identifier
|
|
26
|
+
attr_reader :message_id
|
|
27
|
+
|
|
28
|
+
# @return [Integer] error code (0 means success)
|
|
29
|
+
attr_reader :error_code
|
|
30
|
+
|
|
31
|
+
# @return [String] response message
|
|
32
|
+
attr_reader :message
|
|
33
|
+
|
|
34
|
+
# @return [Hash] raw response data
|
|
35
|
+
attr_reader :raw_response
|
|
36
|
+
|
|
37
|
+
# Initialize from API response hash
|
|
38
|
+
#
|
|
39
|
+
# @param response [Hash] the API response
|
|
40
|
+
def initialize(response)
|
|
41
|
+
@raw_response = response
|
|
42
|
+
@to = response["To"]
|
|
43
|
+
@submitted_at = parse_timestamp(response["SubmittedAt"])
|
|
44
|
+
@message_id = response["MessageID"]
|
|
45
|
+
@error_code = response["ErrorCode"]
|
|
46
|
+
@message = response["Message"]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check if the email was sent successfully
|
|
50
|
+
#
|
|
51
|
+
# @return [Boolean] true if error_code is 0
|
|
52
|
+
def success?
|
|
53
|
+
error_code == 0
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Check if there was an error
|
|
57
|
+
#
|
|
58
|
+
# @return [Boolean] true if error_code is not 0
|
|
59
|
+
def error?
|
|
60
|
+
!success?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get a string representation of the response
|
|
64
|
+
#
|
|
65
|
+
# @return [String] human-readable response summary
|
|
66
|
+
def to_s
|
|
67
|
+
if success?
|
|
68
|
+
"Email sent to #{to} (Message ID: #{message_id})"
|
|
69
|
+
else
|
|
70
|
+
"Error #{error_code}: #{message}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Parse ISO 8601 timestamp string to Time object
|
|
77
|
+
#
|
|
78
|
+
# @param timestamp [String, nil] the timestamp string
|
|
79
|
+
# @return [Time, nil] parsed Time object
|
|
80
|
+
def parse_timestamp(timestamp)
|
|
81
|
+
return nil if timestamp.nil?
|
|
82
|
+
|
|
83
|
+
Time.parse(timestamp)
|
|
84
|
+
rescue ArgumentError
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PostmarkClient
|
|
4
|
+
module Resources
|
|
5
|
+
# Email resource client for sending emails via the Postmark API.
|
|
6
|
+
#
|
|
7
|
+
# @example Sending a simple email
|
|
8
|
+
# emails = PostmarkClient::Resources::Emails.new
|
|
9
|
+
# response = emails.send_email(
|
|
10
|
+
# from: "sender@example.com",
|
|
11
|
+
# to: "recipient@example.com",
|
|
12
|
+
# subject: "Hello!",
|
|
13
|
+
# text_body: "Hello, World!"
|
|
14
|
+
# )
|
|
15
|
+
#
|
|
16
|
+
# @example Sending an email with an Email model
|
|
17
|
+
# email = PostmarkClient::Email.new(
|
|
18
|
+
# from: "sender@example.com",
|
|
19
|
+
# to: "recipient@example.com",
|
|
20
|
+
# subject: "Hello!",
|
|
21
|
+
# html_body: "<h1>Hello, World!</h1>"
|
|
22
|
+
# )
|
|
23
|
+
# response = emails.send(email)
|
|
24
|
+
#
|
|
25
|
+
# @example Sending with a custom API token
|
|
26
|
+
# emails = PostmarkClient::Resources::Emails.new(api_token: "my-token")
|
|
27
|
+
# response = emails.send(email)
|
|
28
|
+
class Emails < Client::Base
|
|
29
|
+
# Send a single email
|
|
30
|
+
#
|
|
31
|
+
# @param email [Email, Hash] the email to send
|
|
32
|
+
# @return [EmailResponse] the API response
|
|
33
|
+
#
|
|
34
|
+
# @raise [ValidationError] if the email is invalid
|
|
35
|
+
# @raise [ApiError] if the API returns an error
|
|
36
|
+
#
|
|
37
|
+
# @example With Email model
|
|
38
|
+
# response = emails.send(email)
|
|
39
|
+
#
|
|
40
|
+
# @example With hash
|
|
41
|
+
# response = emails.send({
|
|
42
|
+
# from: "sender@example.com",
|
|
43
|
+
# to: "recipient@example.com",
|
|
44
|
+
# subject: "Test",
|
|
45
|
+
# text_body: "Hello"
|
|
46
|
+
# })
|
|
47
|
+
def send(email)
|
|
48
|
+
email = normalize_email(email)
|
|
49
|
+
email.validate!
|
|
50
|
+
|
|
51
|
+
response = post("/email", email.to_h)
|
|
52
|
+
EmailResponse.new(response)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Convenience method to send an email with parameters
|
|
56
|
+
#
|
|
57
|
+
# @param from [String] sender email address
|
|
58
|
+
# @param to [String, Array<String>] recipient email address(es)
|
|
59
|
+
# @param subject [String] email subject
|
|
60
|
+
# @param kwargs [Hash] additional email options
|
|
61
|
+
# @option kwargs [String] :html_body HTML email body
|
|
62
|
+
# @option kwargs [String] :text_body plain text email body
|
|
63
|
+
# @option kwargs [String] :cc CC recipients
|
|
64
|
+
# @option kwargs [String] :bcc BCC recipients
|
|
65
|
+
# @option kwargs [String] :reply_to reply-to address
|
|
66
|
+
# @option kwargs [String] :tag email tag
|
|
67
|
+
# @option kwargs [Boolean] :track_opens whether to track opens
|
|
68
|
+
# @option kwargs [String] :track_links link tracking setting
|
|
69
|
+
# @option kwargs [Hash] :metadata custom metadata
|
|
70
|
+
# @option kwargs [String] :message_stream message stream
|
|
71
|
+
# @return [EmailResponse] the API response
|
|
72
|
+
#
|
|
73
|
+
# @example
|
|
74
|
+
# response = emails.send_email(
|
|
75
|
+
# from: "sender@example.com",
|
|
76
|
+
# to: "recipient@example.com",
|
|
77
|
+
# subject: "Hello!",
|
|
78
|
+
# text_body: "Hello, World!",
|
|
79
|
+
# track_opens: true
|
|
80
|
+
# )
|
|
81
|
+
def send_email(from:, to:, subject:, **kwargs)
|
|
82
|
+
email = Email.new(
|
|
83
|
+
from: from,
|
|
84
|
+
to: to,
|
|
85
|
+
subject: subject,
|
|
86
|
+
**kwargs
|
|
87
|
+
)
|
|
88
|
+
send(email)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Send a batch of emails (up to 500)
|
|
92
|
+
#
|
|
93
|
+
# @param emails [Array<Email, Hash>] array of emails to send
|
|
94
|
+
# @return [Array<EmailResponse>] array of API responses
|
|
95
|
+
#
|
|
96
|
+
# @raise [ValidationError] if any email is invalid
|
|
97
|
+
# @raise [ApiError] if the API returns an error
|
|
98
|
+
# @raise [ArgumentError] if batch exceeds 500 emails
|
|
99
|
+
#
|
|
100
|
+
# @example
|
|
101
|
+
# emails_to_send = [
|
|
102
|
+
# { from: "a@example.com", to: "b@example.com", subject: "Hi", text_body: "Hello" },
|
|
103
|
+
# { from: "a@example.com", to: "c@example.com", subject: "Hi", text_body: "Hello" }
|
|
104
|
+
# ]
|
|
105
|
+
# responses = client.send_batch(emails_to_send)
|
|
106
|
+
def send_batch(emails)
|
|
107
|
+
raise ArgumentError, "Batch cannot exceed 500 emails" if emails.length > 500
|
|
108
|
+
|
|
109
|
+
normalized = emails.map { |e| normalize_email(e) }
|
|
110
|
+
normalized.each(&:validate!)
|
|
111
|
+
|
|
112
|
+
payload = normalized.map(&:to_h)
|
|
113
|
+
responses = post("/email/batch", payload)
|
|
114
|
+
|
|
115
|
+
responses.map { |r| EmailResponse.new(r) }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
# Normalize email input to Email model
|
|
121
|
+
#
|
|
122
|
+
# @param email [Email, Hash] the email input
|
|
123
|
+
# @return [Email] normalized email model
|
|
124
|
+
def normalize_email(email)
|
|
125
|
+
return email if email.is_a?(Email)
|
|
126
|
+
|
|
127
|
+
Email.new(**symbolize_keys(email))
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Convert hash keys to symbols
|
|
131
|
+
#
|
|
132
|
+
# @param hash [Hash] hash with string or symbol keys
|
|
133
|
+
# @return [Hash] hash with symbol keys
|
|
134
|
+
def symbolize_keys(hash)
|
|
135
|
+
hash.transform_keys do |key|
|
|
136
|
+
case key
|
|
137
|
+
when "From", :From then :from
|
|
138
|
+
when "To", :To then :to
|
|
139
|
+
when "Cc", :Cc then :cc
|
|
140
|
+
when "Bcc", :Bcc then :bcc
|
|
141
|
+
when "Subject", :Subject then :subject
|
|
142
|
+
when "HtmlBody", :HtmlBody then :html_body
|
|
143
|
+
when "TextBody", :TextBody then :text_body
|
|
144
|
+
when "ReplyTo", :ReplyTo then :reply_to
|
|
145
|
+
when "Tag", :Tag then :tag
|
|
146
|
+
when "Headers", :Headers then :headers
|
|
147
|
+
when "TrackOpens", :TrackOpens then :track_opens
|
|
148
|
+
when "TrackLinks", :TrackLinks then :track_links
|
|
149
|
+
when "Attachments", :Attachments then :attachments
|
|
150
|
+
when "Metadata", :Metadata then :metadata
|
|
151
|
+
when "MessageStream", :MessageStream then :message_stream
|
|
152
|
+
else
|
|
153
|
+
key.to_s.gsub(/([A-Z])/, '_\1').downcase.delete_prefix("_").to_sym
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|