sendlayer 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/.env.example +3 -0
- data/.github/workflows/publish.yml +101 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +27 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +106 -0
- data/LICENSE +21 -0
- data/README.md +222 -0
- data/Rakefile +29 -0
- data/examples/events.rb +58 -0
- data/examples/send_email.rb +56 -0
- data/examples/webhooks.rb +43 -0
- data/lib/sendlayer/client.rb +98 -0
- data/lib/sendlayer/emails.rb +183 -0
- data/lib/sendlayer/events.rb +39 -0
- data/lib/sendlayer/exceptions.rb +26 -0
- data/lib/sendlayer/version.rb +3 -0
- data/lib/sendlayer/webhooks.rb +53 -0
- data/lib/sendlayer.rb +19 -0
- data/sendlayer.gemspec +39 -0
- metadata +166 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: fcbeb3e895d3e5a69471af13feb2c2f85b02c6d8babda4e1417266caa848228d
|
|
4
|
+
data.tar.gz: e82bfac8473f5db810f7d465055b05ca0a92009eafc3c81204a97d0c143c9d00
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: aa169ec23cc27a318feac68e0de833b5c89b9e142406d5bcbe3c365b4961c2d487c4f470a05754f330aacc160dc1a66d3c6c6d95f8cf5aef63bfa30f58945ed5
|
|
7
|
+
data.tar.gz: fa2bdd5529ed366c76b1da611240eb4d026b7dfbec0776537fecd260a1c906ed4d3144f0f6e20314c2d07709fed281b848873f435d36de94d922e41f8170f7f1
|
data/.env.example
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
name: Publish Ruby Gem
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- name: Checkout code
|
|
16
|
+
uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Setup Ruby
|
|
19
|
+
uses: ruby/setup-ruby@v1
|
|
20
|
+
with:
|
|
21
|
+
ruby-version: '3.2'
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: |
|
|
25
|
+
gem install bundler
|
|
26
|
+
bundle install
|
|
27
|
+
|
|
28
|
+
- name: Run tests
|
|
29
|
+
run: bundle exec rspec
|
|
30
|
+
|
|
31
|
+
# - name: Run RuboCop
|
|
32
|
+
# run: bundle exec rubocop
|
|
33
|
+
|
|
34
|
+
publish:
|
|
35
|
+
needs: test
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
|
|
38
|
+
permissions:
|
|
39
|
+
contents: write
|
|
40
|
+
id-token: write
|
|
41
|
+
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/checkout@v4
|
|
44
|
+
with:
|
|
45
|
+
persist-credentials: false
|
|
46
|
+
|
|
47
|
+
- name: Set up Ruby
|
|
48
|
+
uses: ruby/setup-ruby@v1
|
|
49
|
+
with:
|
|
50
|
+
bundler-cache: true
|
|
51
|
+
ruby-version: '3.2'
|
|
52
|
+
|
|
53
|
+
- name: Publish to RubyGems
|
|
54
|
+
uses: rubygems/release-gem@v1
|
|
55
|
+
|
|
56
|
+
github-release:
|
|
57
|
+
needs: publish
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
steps:
|
|
60
|
+
- name: Checkout code
|
|
61
|
+
uses: actions/checkout@v4
|
|
62
|
+
with:
|
|
63
|
+
fetch-depth: 0
|
|
64
|
+
|
|
65
|
+
- name: Setup Ruby
|
|
66
|
+
uses: ruby/setup-ruby@v1
|
|
67
|
+
with:
|
|
68
|
+
ruby-version: '3.2'
|
|
69
|
+
|
|
70
|
+
- name: Install dependencies
|
|
71
|
+
run: |
|
|
72
|
+
gem install bundler
|
|
73
|
+
bundle install
|
|
74
|
+
|
|
75
|
+
- name: Get version from VERSION file
|
|
76
|
+
id: get_version
|
|
77
|
+
run: |
|
|
78
|
+
VERSION=$(cat lib/sendlayer/version.rb | grep "VERSION = " | sed "s/VERSION = '\(.*\)'/\1/")
|
|
79
|
+
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
|
80
|
+
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
81
|
+
|
|
82
|
+
- name: Create and push tag
|
|
83
|
+
run: |
|
|
84
|
+
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
|
85
|
+
git config --local user.name "github-actions[bot]"
|
|
86
|
+
git tag -a v${{ env.VERSION }} -m "Release v${{ env.VERSION }}"
|
|
87
|
+
git push origin v${{ env.VERSION }}
|
|
88
|
+
|
|
89
|
+
- name: Build gem
|
|
90
|
+
run: gem build sendlayer.gemspec
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
- name: Create GitHub Release
|
|
94
|
+
uses: softprops/action-gh-release@v1
|
|
95
|
+
with:
|
|
96
|
+
tag_name: v${{ steps.get_version.outputs.version }}
|
|
97
|
+
name: Release v${{ steps.get_version.outputs.version }}
|
|
98
|
+
files: sendlayer-${{ steps.get_version.outputs.version }}.gem
|
|
99
|
+
generate_release_notes: true
|
|
100
|
+
env:
|
|
101
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
TargetRubyVersion: 2.7
|
|
3
|
+
NewCops: enable
|
|
4
|
+
|
|
5
|
+
Style/Documentation:
|
|
6
|
+
Enabled: false
|
|
7
|
+
|
|
8
|
+
Style/StringLiterals:
|
|
9
|
+
EnforcedStyle: double_quotes
|
|
10
|
+
|
|
11
|
+
Style/FrozenStringLiteralComment:
|
|
12
|
+
Enabled: true
|
|
13
|
+
|
|
14
|
+
Layout/LineLength:
|
|
15
|
+
Max: 120
|
|
16
|
+
|
|
17
|
+
Metrics/MethodLength:
|
|
18
|
+
Max: 20
|
|
19
|
+
|
|
20
|
+
Metrics/AbcSize:
|
|
21
|
+
Max: 20
|
|
22
|
+
|
|
23
|
+
Style/ClassAndModuleChildren:
|
|
24
|
+
Enabled: false
|
|
25
|
+
|
|
26
|
+
Style/WordArray:
|
|
27
|
+
MinSize: 3
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
sendlayer (1.0.0)
|
|
5
|
+
mime-types (~> 3.4)
|
|
6
|
+
|
|
7
|
+
GEM
|
|
8
|
+
remote: https://rubygems.org/
|
|
9
|
+
specs:
|
|
10
|
+
addressable (2.8.7)
|
|
11
|
+
public_suffix (>= 2.0.2, < 7.0)
|
|
12
|
+
ast (2.4.3)
|
|
13
|
+
bigdecimal (3.3.0)
|
|
14
|
+
byebug (12.0.0)
|
|
15
|
+
coderay (1.1.3)
|
|
16
|
+
crack (1.0.0)
|
|
17
|
+
bigdecimal
|
|
18
|
+
rexml
|
|
19
|
+
diff-lcs (1.6.2)
|
|
20
|
+
docile (1.4.1)
|
|
21
|
+
hashdiff (1.2.1)
|
|
22
|
+
json (2.15.1)
|
|
23
|
+
language_server-protocol (3.17.0.5)
|
|
24
|
+
lint_roller (1.1.0)
|
|
25
|
+
logger (1.7.0)
|
|
26
|
+
method_source (1.1.0)
|
|
27
|
+
mime-types (3.7.0)
|
|
28
|
+
logger
|
|
29
|
+
mime-types-data (~> 3.2025, >= 3.2025.0507)
|
|
30
|
+
mime-types-data (3.2025.0924)
|
|
31
|
+
parallel (1.27.0)
|
|
32
|
+
parser (3.3.9.0)
|
|
33
|
+
ast (~> 2.4.1)
|
|
34
|
+
racc
|
|
35
|
+
prism (1.5.1)
|
|
36
|
+
pry (0.15.2)
|
|
37
|
+
coderay (~> 1.1)
|
|
38
|
+
method_source (~> 1.0)
|
|
39
|
+
pry-byebug (3.11.0)
|
|
40
|
+
byebug (~> 12.0)
|
|
41
|
+
pry (>= 0.13, < 0.16)
|
|
42
|
+
public_suffix (6.0.2)
|
|
43
|
+
racc (1.8.1)
|
|
44
|
+
rainbow (3.1.1)
|
|
45
|
+
rake (13.3.0)
|
|
46
|
+
regexp_parser (2.11.3)
|
|
47
|
+
rexml (3.4.4)
|
|
48
|
+
rspec (3.13.1)
|
|
49
|
+
rspec-core (~> 3.13.0)
|
|
50
|
+
rspec-expectations (~> 3.13.0)
|
|
51
|
+
rspec-mocks (~> 3.13.0)
|
|
52
|
+
rspec-core (3.13.5)
|
|
53
|
+
rspec-support (~> 3.13.0)
|
|
54
|
+
rspec-expectations (3.13.5)
|
|
55
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
56
|
+
rspec-support (~> 3.13.0)
|
|
57
|
+
rspec-mocks (3.13.5)
|
|
58
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
59
|
+
rspec-support (~> 3.13.0)
|
|
60
|
+
rspec-support (3.13.6)
|
|
61
|
+
rubocop (1.81.1)
|
|
62
|
+
json (~> 2.3)
|
|
63
|
+
language_server-protocol (~> 3.17.0.2)
|
|
64
|
+
lint_roller (~> 1.1.0)
|
|
65
|
+
parallel (~> 1.10)
|
|
66
|
+
parser (>= 3.3.0.2)
|
|
67
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
68
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
|
69
|
+
rubocop-ast (>= 1.47.1, < 2.0)
|
|
70
|
+
ruby-progressbar (~> 1.7)
|
|
71
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
|
72
|
+
rubocop-ast (1.47.1)
|
|
73
|
+
parser (>= 3.3.7.2)
|
|
74
|
+
prism (~> 1.4)
|
|
75
|
+
ruby-progressbar (1.13.0)
|
|
76
|
+
simplecov (0.22.0)
|
|
77
|
+
docile (~> 1.1)
|
|
78
|
+
simplecov-html (~> 0.11)
|
|
79
|
+
simplecov_json_formatter (~> 0.1)
|
|
80
|
+
simplecov-html (0.13.2)
|
|
81
|
+
simplecov_json_formatter (0.1.4)
|
|
82
|
+
unicode-display_width (3.2.0)
|
|
83
|
+
unicode-emoji (~> 4.1)
|
|
84
|
+
unicode-emoji (4.1.0)
|
|
85
|
+
webmock (3.25.1)
|
|
86
|
+
addressable (>= 2.8.0)
|
|
87
|
+
crack (>= 0.3.2)
|
|
88
|
+
hashdiff (>= 0.4.0, < 2.0.0)
|
|
89
|
+
|
|
90
|
+
PLATFORMS
|
|
91
|
+
arm64-darwin-22
|
|
92
|
+
x86_64-linux
|
|
93
|
+
|
|
94
|
+
DEPENDENCIES
|
|
95
|
+
bundler (~> 2.0)
|
|
96
|
+
pry (~> 0.14)
|
|
97
|
+
pry-byebug (~> 3.10)
|
|
98
|
+
rake (~> 13.0)
|
|
99
|
+
rspec (~> 3.0)
|
|
100
|
+
rubocop (~> 1.0)
|
|
101
|
+
sendlayer!
|
|
102
|
+
simplecov (~> 0.21)
|
|
103
|
+
webmock (~> 3.0)
|
|
104
|
+
|
|
105
|
+
BUNDLED WITH
|
|
106
|
+
2.4.19
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 SendLayer
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
<a href="https://sendlayer.com">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: light)" srcset="https://sendlayer.com/wp-content/themes/sendlayer-theme/assets/images/svg/logo-dark.svg">
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://sendlayer.com/wp-content/themes/sendlayer-theme/assets/images/svg/logo-light.svg">
|
|
5
|
+
<img alt="SendLayer Logo" width="200px" src="https://sendlayer.com/wp-content/themes/sendlayer-theme/assets/images/svg/logo-light.svg">
|
|
6
|
+
</picture>
|
|
7
|
+
</a>
|
|
8
|
+
|
|
9
|
+
### SendLayer Ruby SDK
|
|
10
|
+
|
|
11
|
+
The official Ruby SDK for interacting with the SendLayer API, providing a simple and intuitive interface for sending emails, managing webhooks, and retrieving email events.
|
|
12
|
+
|
|
13
|
+
[](./LICENSE)
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Add this line to your application's Gemfile:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem 'sendlayer'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
And then execute:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bundle install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or install it yourself as:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
gem install sendlayer
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
require 'sendlayer'
|
|
39
|
+
|
|
40
|
+
# Initialize the SDK
|
|
41
|
+
sendlayer = SendLayer::SendLayer.new('your-api-key')
|
|
42
|
+
|
|
43
|
+
# Send an email
|
|
44
|
+
response = sendlayer.emails.send(
|
|
45
|
+
from: 'sender@example.com',
|
|
46
|
+
to: 'recipient@example.com',
|
|
47
|
+
subject: 'Test Email',
|
|
48
|
+
text: 'This is a test email'
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
puts "Email sent! Message ID: #{response['MessageID']}"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Features
|
|
55
|
+
|
|
56
|
+
- **Email Module**: Send emails with HTML/text content, attachments, CC/BCC, reply-to, custom headers, and tags
|
|
57
|
+
- **Webhooks Module**: Create, retrieve, and delete webhooks for various email events
|
|
58
|
+
- **Events Module**: Retrieve email events with filtering options
|
|
59
|
+
- **Error Handling**: Clear, typed errors for API and validation issues
|
|
60
|
+
|
|
61
|
+
## Email
|
|
62
|
+
|
|
63
|
+
Send emails using the `SendLayer` client:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
require 'sendlayer'
|
|
67
|
+
|
|
68
|
+
sendlayer = SendLayer::SendLayer.new('your-api-key')
|
|
69
|
+
|
|
70
|
+
# Simple email
|
|
71
|
+
response = sendlayer.emails.send(
|
|
72
|
+
from: 'sender@example.com',
|
|
73
|
+
to: 'recipient@example.com',
|
|
74
|
+
subject: 'Welcome!',
|
|
75
|
+
text: 'Welcome to our platform!'
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# HTML email with sender name
|
|
79
|
+
response = sendlayer.emails.send(
|
|
80
|
+
from: { email: 'sender@example.com', name: 'Paulie Paloma' },
|
|
81
|
+
to: 'recipient@example.com',
|
|
82
|
+
subject: 'Welcome!',
|
|
83
|
+
html: '<h1>Welcome!</h1><p>Welcome to our platform!</p>'
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Complex email with multiple recipients and attachments
|
|
87
|
+
response = sendlayer.emails.send(
|
|
88
|
+
from: { email: 'sender@example.com', name: 'Sender' },
|
|
89
|
+
to: [
|
|
90
|
+
{ email: 'recipient1@example.com', name: 'Recipient 1' },
|
|
91
|
+
{ email: 'recipient2@example.com', name: 'Recipient 2' }
|
|
92
|
+
],
|
|
93
|
+
subject: 'Complex Email',
|
|
94
|
+
text: 'Plain text fallback',
|
|
95
|
+
html: '<p>This is a <strong>test email</strong>!</p>',
|
|
96
|
+
cc: [{ email: 'cc@example.com', name: 'CC' }],
|
|
97
|
+
bcc: [{ email: 'bcc@example.com', name: 'BCC' }],
|
|
98
|
+
reply_to: [{ email: 'reply@example.com', name: 'Reply' }],
|
|
99
|
+
attachments: [
|
|
100
|
+
{ path: 'path/to/file.pdf', type: 'application/pdf' }
|
|
101
|
+
],
|
|
102
|
+
headers: { 'X-Custom-Header' => 'value' },
|
|
103
|
+
tags: ['tag1', 'tag2']
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Events
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
require 'sendlayer'
|
|
111
|
+
require 'time'
|
|
112
|
+
|
|
113
|
+
sendlayer = SendLayer::SendLayer.new('your-api-key')
|
|
114
|
+
|
|
115
|
+
# Get all events
|
|
116
|
+
all_events = sendlayer.events.get
|
|
117
|
+
puts "Total events: #{all_events['TotalRecords']}"
|
|
118
|
+
|
|
119
|
+
# Get filtered events (last 24 hours, opened)
|
|
120
|
+
end_time = Time.now
|
|
121
|
+
start_time = end_time - (24 * 60 * 60) # 24 hours ago
|
|
122
|
+
|
|
123
|
+
filtered_events = sendlayer.events.get(
|
|
124
|
+
start_date: start_time,
|
|
125
|
+
end_date: end_time,
|
|
126
|
+
event: 'opened'
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
puts "Filtered events: #{filtered_events['TotalRecords']}"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Webhooks
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
require 'sendlayer'
|
|
136
|
+
|
|
137
|
+
sendlayer = SendLayer::SendLayer.new('your-api-key')
|
|
138
|
+
|
|
139
|
+
# Create a webhook
|
|
140
|
+
webhook = sendlayer.webhooks.create(
|
|
141
|
+
url: 'https://your-domain.com/webhook',
|
|
142
|
+
event: 'open'
|
|
143
|
+
)
|
|
144
|
+
puts "Webhook created: #{webhook['WebhookID']}"
|
|
145
|
+
|
|
146
|
+
# Get all webhooks
|
|
147
|
+
webhooks = sendlayer.webhooks.get
|
|
148
|
+
puts "Webhooks: #{webhooks}"
|
|
149
|
+
|
|
150
|
+
# Delete a webhook
|
|
151
|
+
sendlayer.webhooks.delete(123)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Error Handling
|
|
155
|
+
|
|
156
|
+
The SDK provides specific exception types for different error scenarios:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
require 'sendlayer'
|
|
160
|
+
|
|
161
|
+
begin
|
|
162
|
+
response = sendlayer.emails.send(
|
|
163
|
+
from: 'sender@example.com',
|
|
164
|
+
to: 'recipient@example.com',
|
|
165
|
+
subject: 'Test Email',
|
|
166
|
+
text: 'This is a test email'
|
|
167
|
+
)
|
|
168
|
+
rescue SendLayer::SendLayerAPIError => e
|
|
169
|
+
puts "API error: #{e.message} (Status: #{e.status_code})"
|
|
170
|
+
rescue SendLayer::SendLayerValidationError => e
|
|
171
|
+
puts "Validation error: #{e.message}"
|
|
172
|
+
rescue SendLayer::SendLayerAuthenticationError => e
|
|
173
|
+
puts "Authentication error: #{e.message}"
|
|
174
|
+
rescue SendLayer::SendLayerError => e
|
|
175
|
+
puts "SendLayer error: #{e.message}"
|
|
176
|
+
rescue => e
|
|
177
|
+
puts "Unexpected error: #{e.message}"
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Exception Types
|
|
182
|
+
|
|
183
|
+
- `SendLayer::SendLayerError`: Base exception for all SendLayer errors
|
|
184
|
+
- `SendLayer::SendLayerAPIError`: API-specific errors with status code and response data
|
|
185
|
+
- `SendLayer::SendLayerAuthenticationError`: Invalid API key or authentication issues
|
|
186
|
+
- `SendLayer::SendLayerValidationError`: Invalid parameters or validation errors
|
|
187
|
+
- `SendLayer::SendLayerNotFoundError`: Resource not found (404 errors)
|
|
188
|
+
- `SendLayer::SendLayerRateLimitError`: Rate limit exceeded (429 errors)
|
|
189
|
+
- `SendLayer::SendLayerInternalServerError`: Server errors (5xx errors)
|
|
190
|
+
|
|
191
|
+
## Supported Events
|
|
192
|
+
|
|
193
|
+
### Webhook Events
|
|
194
|
+
- `bounce`: Email bounced
|
|
195
|
+
- `click`: Link was clicked
|
|
196
|
+
- `open`: Email was opened
|
|
197
|
+
- `unsubscribe`: User unsubscribed
|
|
198
|
+
- `complaint`: User marked as spam
|
|
199
|
+
- `delivery`: Email was delivered
|
|
200
|
+
|
|
201
|
+
### Event Tracking Events
|
|
202
|
+
- `accepted`: Email was accepted by the server
|
|
203
|
+
- `rejected`: Email was rejected
|
|
204
|
+
- `delivered`: Email was delivered
|
|
205
|
+
- `opened`: Email was opened
|
|
206
|
+
- `clicked`: Link was clicked
|
|
207
|
+
- `unsubscribed`: User unsubscribed
|
|
208
|
+
- `complained`: User marked as spam
|
|
209
|
+
- `failed`: Email delivery failed
|
|
210
|
+
|
|
211
|
+
## Requirements
|
|
212
|
+
|
|
213
|
+
- Ruby 2.7.0 or higher
|
|
214
|
+
- mime-types gem
|
|
215
|
+
|
|
216
|
+
## More Details
|
|
217
|
+
|
|
218
|
+
To learn more about using the SendLayer SDK, be sure to check our [Developer Documentation](https://developers.sendlayer.com/sdks/ruby).
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
MIT License - see [LICENSE](./LICENSE) file for details
|
data/Rakefile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require 'bundler/gem_tasks'
|
|
2
|
+
require 'rspec/core/rake_task'
|
|
3
|
+
require 'rubocop/rake_task'
|
|
4
|
+
|
|
5
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
6
|
+
|
|
7
|
+
RuboCop::RakeTask.new
|
|
8
|
+
|
|
9
|
+
task default: [:spec, :rubocop]
|
|
10
|
+
|
|
11
|
+
desc 'Run all tests and code quality checks'
|
|
12
|
+
task ci: [:spec, :rubocop]
|
|
13
|
+
|
|
14
|
+
desc 'Build the gem'
|
|
15
|
+
task :build do
|
|
16
|
+
sh 'gem build sendlayer.gemspec'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
desc 'Install the gem locally'
|
|
20
|
+
task install: :build do
|
|
21
|
+
sh 'gem install sendlayer-*.gem'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
desc 'Clean up generated files'
|
|
25
|
+
task :clean do
|
|
26
|
+
sh 'rm -f sendlayer-*.gem'
|
|
27
|
+
sh 'rm -rf coverage/'
|
|
28
|
+
sh 'rm -rf .rspec_status'
|
|
29
|
+
end
|
data/examples/events.rb
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'sendlayer'
|
|
4
|
+
|
|
5
|
+
# Initialize the SDK
|
|
6
|
+
api_key = ENV['SENDLAYER_API_KEY']
|
|
7
|
+
unless api_key
|
|
8
|
+
puts "Please set SENDLAYER_API_KEY environment variable"
|
|
9
|
+
exit 1
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
sendlayer = SendLayer::SendLayer.new(api_key)
|
|
13
|
+
|
|
14
|
+
begin
|
|
15
|
+
# Get all events
|
|
16
|
+
puts "Retrieving all events..."
|
|
17
|
+
all_events = sendlayer.events.get
|
|
18
|
+
puts "✅ Retrieved events successfully!"
|
|
19
|
+
puts "Total events: #{all_events}"
|
|
20
|
+
|
|
21
|
+
# Get filtered events (last 7 days)
|
|
22
|
+
puts "\nRetrieving events from the last 7 days..."
|
|
23
|
+
end_time = Time.now
|
|
24
|
+
start_time = end_time - (7 * 24 * 60 * 60) # 7 days ago
|
|
25
|
+
|
|
26
|
+
filtered_events = sendlayer.events.get(
|
|
27
|
+
start_date: start_time,
|
|
28
|
+
end_date: end_time
|
|
29
|
+
)
|
|
30
|
+
puts "✅ Retrieved filtered events successfully!"
|
|
31
|
+
puts "Events in last 7 days: #{filtered_events['Events']}"
|
|
32
|
+
|
|
33
|
+
# Get specific event type (opened)
|
|
34
|
+
puts "\nRetrieving 'opened' events from the last 24 hours..."
|
|
35
|
+
end_time = Time.now
|
|
36
|
+
start_time = end_time - (24 * 60 * 60) # 24 hours ago
|
|
37
|
+
|
|
38
|
+
opened_events = sendlayer.events.get(
|
|
39
|
+
start_date: start_time,
|
|
40
|
+
end_date: end_time,
|
|
41
|
+
event: 'opened'
|
|
42
|
+
)
|
|
43
|
+
puts "✅ Retrieved 'opened' events successfully!"
|
|
44
|
+
puts "Opened events in last 24 hours: #{opened_events['TotalRecords']}"
|
|
45
|
+
|
|
46
|
+
# Display some sample events
|
|
47
|
+
if all_events['Events'] && !all_events['Events'].empty?
|
|
48
|
+
puts "\nSample events:"
|
|
49
|
+
all_events['Events'].first(3).each_with_index do |event, index|
|
|
50
|
+
puts " #{index + 1}. Event: #{event['Event']}, MessageID: #{event['MessageID']}, Timestamp: #{event['Timestamp']}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
rescue SendLayer::SendLayerError => e
|
|
55
|
+
puts "❌ SendLayer Error: #{e.message}"
|
|
56
|
+
rescue => e
|
|
57
|
+
puts "❌ Unexpected Error: #{e.message}"
|
|
58
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'sendlayer'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Initialize the SDK
|
|
7
|
+
api_key = ENV['SENDLAYER_API_KEY']
|
|
8
|
+
unless api_key
|
|
9
|
+
puts "Please set SENDLAYER_API_KEY environment variable"
|
|
10
|
+
exit 1
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
sendlayer = SendLayer::SendLayer.new(api_key)
|
|
14
|
+
|
|
15
|
+
begin
|
|
16
|
+
# Send a simple email
|
|
17
|
+
puts "Sending a simple email..."
|
|
18
|
+
response = sendlayer.emails.send(
|
|
19
|
+
from: 'paulie@example.com',
|
|
20
|
+
to: 'pattie@example.com',
|
|
21
|
+
subject: 'Ruby SDK Test Email',
|
|
22
|
+
text: 'This is a test email sent using the SendLayer Ruby SDK!'
|
|
23
|
+
)
|
|
24
|
+
puts "✅ Email sent successfully!"
|
|
25
|
+
puts "Message ID: #{response}"
|
|
26
|
+
|
|
27
|
+
# Send an HTML email with sender name
|
|
28
|
+
puts "\nSending an HTML email..."
|
|
29
|
+
response = sendlayer.emails.send(
|
|
30
|
+
from: { email: 'paulie@example.com', name: 'Paulie Paloma' },
|
|
31
|
+
to: [
|
|
32
|
+
{name: 'Pattie Paloma', email:'pattie@example.com'},
|
|
33
|
+
{name: 'John Doe', email:'john@example.com'}
|
|
34
|
+
],
|
|
35
|
+
subject: 'HTML Email Test',
|
|
36
|
+
html: '<h1>Hello from Ruby!</h1><p>This is an <strong>HTML</strong> email sent using the SendLayer Ruby SDK.</p>',
|
|
37
|
+
text: 'Hello from Ruby! This is an HTML email sent using the SendLayer Ruby SDK.',
|
|
38
|
+
attachments: [
|
|
39
|
+
{
|
|
40
|
+
path: './path/to/attachment.pdf',
|
|
41
|
+
type: 'application/pdf'
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
path: 'https://placehold.co/600x400.png',
|
|
45
|
+
type: 'image/png'
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
)
|
|
49
|
+
puts "✅ HTML email sent successfully!"
|
|
50
|
+
puts "Message ID: #{response}"
|
|
51
|
+
|
|
52
|
+
rescue SendLayer::SendLayerError => e
|
|
53
|
+
puts "❌ SendLayer Error: #{e.message}"
|
|
54
|
+
rescue => e
|
|
55
|
+
puts "❌ Unexpected Error: #{e.message}"
|
|
56
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'sendlayer'
|
|
4
|
+
|
|
5
|
+
# Initialize the SDK
|
|
6
|
+
api_key = ENV['SENDLAYER_API_KEY']
|
|
7
|
+
unless api_key
|
|
8
|
+
puts "Please set SENDLAYER_API_KEY environment variable"
|
|
9
|
+
exit 1
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
sendlayer = SendLayer::SendLayer.new(api_key)
|
|
13
|
+
|
|
14
|
+
begin
|
|
15
|
+
# Create a webhook
|
|
16
|
+
puts "Creating a webhook..."
|
|
17
|
+
webhook = sendlayer.webhooks.create(
|
|
18
|
+
url: 'https://example.com/webhook',
|
|
19
|
+
event: 'open'
|
|
20
|
+
)
|
|
21
|
+
puts "✅ Webhook created successfully!"
|
|
22
|
+
puts "Webhook ID: #{webhook}"
|
|
23
|
+
|
|
24
|
+
# Get all webhooks
|
|
25
|
+
puts "\nRetrieving all webhooks..."
|
|
26
|
+
webhooks = sendlayer.webhooks.get
|
|
27
|
+
puts "✅ Retrieved #{webhooks.length} webhook(s)"
|
|
28
|
+
webhooks.each_with_index do |webhook, index|
|
|
29
|
+
puts " Webhooks: #{webhooks}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Delete the webhook we just created
|
|
33
|
+
# if webhook['WebhookID']
|
|
34
|
+
puts "\nDeleting webhook..."
|
|
35
|
+
sendlayer.webhooks.delete(27712)
|
|
36
|
+
puts "✅ Webhook deleted successfully!"
|
|
37
|
+
# end
|
|
38
|
+
|
|
39
|
+
rescue SendLayer::SendLayerError => e
|
|
40
|
+
puts "❌ SendLayer Error: #{e.message}"
|
|
41
|
+
rescue => e
|
|
42
|
+
puts "❌ Unexpected Error: #{e.message}"
|
|
43
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require 'uri'
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
|
|
6
|
+
module SendLayer
|
|
7
|
+
class Client
|
|
8
|
+
BASE_URL = 'https://console.sendlayer.com/api/v1/'
|
|
9
|
+
|
|
10
|
+
attr_reader :api_key, :base_url, :timeout, :attachment_url_timeout
|
|
11
|
+
|
|
12
|
+
def initialize(api_key, options = {})
|
|
13
|
+
@api_key = api_key
|
|
14
|
+
@base_url = options[:base_url] || BASE_URL
|
|
15
|
+
@timeout = options[:timeout] || 30
|
|
16
|
+
@attachment_url_timeout = options[:attachment_url_timeout] || 30000
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def make_request(method, endpoint, data = nil, params = {})
|
|
20
|
+
uri = URI("#{@base_url}#{endpoint}")
|
|
21
|
+
|
|
22
|
+
# Add query parameters
|
|
23
|
+
unless params.empty?
|
|
24
|
+
uri.query = URI.encode_www_form(params)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
28
|
+
http.use_ssl = true
|
|
29
|
+
http.read_timeout = @timeout
|
|
30
|
+
|
|
31
|
+
case method.upcase
|
|
32
|
+
when 'GET'
|
|
33
|
+
request = Net::HTTP::Get.new(uri)
|
|
34
|
+
when 'POST'
|
|
35
|
+
request = Net::HTTP::Post.new(uri)
|
|
36
|
+
request.body = data.to_json if data
|
|
37
|
+
request['Content-Type'] = 'application/json'
|
|
38
|
+
when 'DELETE'
|
|
39
|
+
request = Net::HTTP::Delete.new(uri)
|
|
40
|
+
else
|
|
41
|
+
raise SendLayerError.new("Unsupported HTTP method: #{method}")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
request['Authorization'] = "Bearer #{@api_key}"
|
|
45
|
+
request['User-Agent'] = "SendLayer-Ruby/#{::SendLayer::VERSION}"
|
|
46
|
+
|
|
47
|
+
begin
|
|
48
|
+
response = http.request(request)
|
|
49
|
+
handle_response(response)
|
|
50
|
+
rescue Timeout::Error
|
|
51
|
+
raise SendLayerError.new("Request timeout after #{@timeout} seconds")
|
|
52
|
+
rescue => e
|
|
53
|
+
raise SendLayerError.new("Network error: #{e.message}")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def handle_response(response)
|
|
60
|
+
case response.code.to_i
|
|
61
|
+
when 200..299
|
|
62
|
+
return JSON.parse(response.body) if response.body && !response.body.empty?
|
|
63
|
+
return {}
|
|
64
|
+
when 401
|
|
65
|
+
raise SendLayerAuthenticationError.new("Invalid API key")
|
|
66
|
+
when 400
|
|
67
|
+
error_data = parse_error_response(response)
|
|
68
|
+
raise SendLayerValidationError.new(error_data['Error'] || 'Invalid request parameters')
|
|
69
|
+
when 404
|
|
70
|
+
error_data = parse_error_response(response)
|
|
71
|
+
raise SendLayerNotFoundError.new(error_data['Error'] || 'Resource not found')
|
|
72
|
+
when 422
|
|
73
|
+
error_data = parse_error_response(response)
|
|
74
|
+
raise SendLayerValidationError.new(error_data['Error'] || 'Validation failed')
|
|
75
|
+
when 429
|
|
76
|
+
error_data = parse_error_response(response)
|
|
77
|
+
raise SendLayerRateLimitError.new(error_data['Error'] || 'Rate limit exceeded')
|
|
78
|
+
when 500..599
|
|
79
|
+
error_data = parse_error_response(response)
|
|
80
|
+
raise SendLayerInternalServerError.new(error_data['Error'] || 'Internal server error')
|
|
81
|
+
else
|
|
82
|
+
error_data = parse_error_response(response)
|
|
83
|
+
raise SendLayerAPIError.new(
|
|
84
|
+
error_data['Error'] || "HTTP #{response.code} error",
|
|
85
|
+
response.code.to_i,
|
|
86
|
+
response.body
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def parse_error_response(response)
|
|
92
|
+
return {} unless response.body
|
|
93
|
+
JSON.parse(response.body)
|
|
94
|
+
rescue JSON::ParserError
|
|
95
|
+
{ 'Error' => response.body }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require 'uri'
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
|
|
6
|
+
# Optional dependency for MIME type detection
|
|
7
|
+
begin
|
|
8
|
+
require 'mime/types'
|
|
9
|
+
MIME_TYPES_AVAILABLE = true
|
|
10
|
+
rescue LoadError
|
|
11
|
+
MIME_TYPES_AVAILABLE = false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module SendLayer
|
|
15
|
+
class Emails
|
|
16
|
+
def initialize(client)
|
|
17
|
+
@client = client
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def send(from:, to:, subject:, text: nil, html: nil, cc: nil, bcc: nil, reply_to: nil,
|
|
21
|
+
attachments: nil, headers: nil, tags: nil)
|
|
22
|
+
|
|
23
|
+
# Validate required parameters
|
|
24
|
+
raise SendLayerValidationError.new("Either 'text' or 'html' content must be provided") if text.nil? && html.nil?
|
|
25
|
+
|
|
26
|
+
# Prepare email data
|
|
27
|
+
email_data = {
|
|
28
|
+
From: normalize_recipient(from, 'sender'),
|
|
29
|
+
To: normalize_recipients(to, 'recipient'),
|
|
30
|
+
Subject: subject
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if html
|
|
34
|
+
email_data[:ContentType] = "HTML"
|
|
35
|
+
email_data[:HTMLContent] = html
|
|
36
|
+
else
|
|
37
|
+
email_data[:ContentType] = "Text"
|
|
38
|
+
email_data[:PlainContent] = text
|
|
39
|
+
end
|
|
40
|
+
email_data[:CC] = normalize_recipients(cc) if cc
|
|
41
|
+
email_data[:BCC] = normalize_recipients(bcc) if bcc
|
|
42
|
+
email_data[:ReplyTo] = normalize_recipients(reply_to, 'reply_to') if reply_to
|
|
43
|
+
email_data[:Headers] = headers if headers
|
|
44
|
+
|
|
45
|
+
if tags
|
|
46
|
+
unless tags.is_a?(Array) && tags.all? { |t| t.is_a?(String) }
|
|
47
|
+
raise SendLayerValidationError.new('Tags must be a list of strings')
|
|
48
|
+
end
|
|
49
|
+
email_data[:Tags] = tags
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Handle attachments
|
|
53
|
+
if attachments && !attachments.empty?
|
|
54
|
+
email_data[:Attachments] = process_attachments(attachments)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
@client.make_request('POST', 'email', email_data)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def normalize_recipient(recipient, role = 'recipient')
|
|
63
|
+
case recipient
|
|
64
|
+
when String
|
|
65
|
+
validate_email(recipient, role)
|
|
66
|
+
{ email: recipient }
|
|
67
|
+
when Hash
|
|
68
|
+
validate_email(recipient[:email] || recipient['email'], role)
|
|
69
|
+
{
|
|
70
|
+
email: recipient[:email] || recipient['email'],
|
|
71
|
+
name: recipient[:name] || recipient['name']
|
|
72
|
+
}.compact
|
|
73
|
+
else
|
|
74
|
+
raise SendLayerValidationError.new("Invalid #{role} format: #{recipient.class}")
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def normalize_recipients(recipients, role = 'recipient')
|
|
79
|
+
return nil if recipients.nil?
|
|
80
|
+
|
|
81
|
+
case recipients
|
|
82
|
+
when String
|
|
83
|
+
[normalize_recipient(recipients, role)]
|
|
84
|
+
when Array
|
|
85
|
+
recipients.map { |r| normalize_recipient(r, role) }
|
|
86
|
+
else
|
|
87
|
+
raise SendLayerValidationError.new("Invalid #{role}s format: #{recipients.class}")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def validate_email(email, role = 'recipient')
|
|
92
|
+
email_regex = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
|
|
93
|
+
unless email_regex.match?(email)
|
|
94
|
+
raise SendLayerValidationError.new("Invalid #{role} email address: #{email}")
|
|
95
|
+
end
|
|
96
|
+
email
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def process_attachments(attachments)
|
|
100
|
+
attachments.map do |attachment|
|
|
101
|
+
case attachment
|
|
102
|
+
when Hash
|
|
103
|
+
path = attachment[:path] || attachment['path']
|
|
104
|
+
type = attachment[:type] || attachment['type']
|
|
105
|
+
if path.nil? || (path.respond_to?(:strip) && path.strip.empty?)
|
|
106
|
+
raise SendLayerValidationError.new("Attachment path is required")
|
|
107
|
+
end
|
|
108
|
+
encoded_content = read_attachment_content(path)
|
|
109
|
+
{
|
|
110
|
+
Content: encoded_content,
|
|
111
|
+
Type: type || detect_content_type(path),
|
|
112
|
+
Filename: File.basename(path),
|
|
113
|
+
Disposition: "attachment",
|
|
114
|
+
ContentID: SecureRandom.random_number(2**31 - 1)
|
|
115
|
+
}
|
|
116
|
+
when String
|
|
117
|
+
encoded_content = read_attachment_content(attachment)
|
|
118
|
+
{
|
|
119
|
+
Content: encoded_content,
|
|
120
|
+
Type: detect_content_type(attachment),
|
|
121
|
+
Filename: File.basename(attachment),
|
|
122
|
+
Disposition: "attachment",
|
|
123
|
+
ContentID: SecureRandom.random_number(2**31 - 1)
|
|
124
|
+
}
|
|
125
|
+
else
|
|
126
|
+
raise SendLayerValidationError.new("Invalid attachment format: #{attachment.class}")
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def read_attachment_content(path)
|
|
132
|
+
uri = URI.parse(path) rescue nil
|
|
133
|
+
is_url = uri && (uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS))
|
|
134
|
+
|
|
135
|
+
if is_url
|
|
136
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
137
|
+
http.use_ssl = uri.is_a?(URI::HTTPS)
|
|
138
|
+
timeout_ms = (@client.attachment_url_timeout || 30000)
|
|
139
|
+
http.read_timeout = (timeout_ms.to_f / 1000.0)
|
|
140
|
+
request = Net::HTTP::Get.new(uri)
|
|
141
|
+
response = http.request(request)
|
|
142
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
143
|
+
raise SendLayerValidationError.new("Error fetching remote file: HTTP #{response.code}")
|
|
144
|
+
end
|
|
145
|
+
return Base64.strict_encode64(response.body)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
unless File.exist?(path)
|
|
149
|
+
raise SendLayerValidationError.new("Attachment file not found: #{path}")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
content = File.binread(path)
|
|
153
|
+
Base64.strict_encode64(content)
|
|
154
|
+
rescue => e
|
|
155
|
+
raise SendLayerValidationError.new("Error reading attachment: #{e.message}")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def detect_content_type(path)
|
|
159
|
+
if MIME_TYPES_AVAILABLE
|
|
160
|
+
mime_type = MIME::Types.type_for(path).first
|
|
161
|
+
mime_type ? mime_type.content_type : 'application/octet-stream'
|
|
162
|
+
else
|
|
163
|
+
# Fallback to basic file extension detection
|
|
164
|
+
case File.extname(path).downcase
|
|
165
|
+
when '.pdf'
|
|
166
|
+
'application/pdf'
|
|
167
|
+
when '.txt'
|
|
168
|
+
'text/plain'
|
|
169
|
+
when '.html', '.htm'
|
|
170
|
+
'text/html'
|
|
171
|
+
when '.jpg', '.jpeg'
|
|
172
|
+
'image/jpeg'
|
|
173
|
+
when '.png'
|
|
174
|
+
'image/png'
|
|
175
|
+
when '.gif'
|
|
176
|
+
'image/gif'
|
|
177
|
+
else
|
|
178
|
+
'application/octet-stream'
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module SendLayer
|
|
2
|
+
class Events
|
|
3
|
+
VALID_EVENTS = %w[accepted rejected delivered opened clicked unsubscribed complained failed].freeze
|
|
4
|
+
|
|
5
|
+
def initialize(client)
|
|
6
|
+
@client = client
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def get(start_date: nil, end_date: nil, event: nil, message_id: nil, start_from: nil, retrieve_count: nil)
|
|
10
|
+
validate_events_params(start_date, end_date, event, retrieve_count)
|
|
11
|
+
|
|
12
|
+
params = {}
|
|
13
|
+
params[:StartDate] = start_date.to_i if start_date
|
|
14
|
+
params[:EndDate] = end_date.to_i if end_date
|
|
15
|
+
params[:Event] = event if event
|
|
16
|
+
params[:MessageID] = message_id if message_id
|
|
17
|
+
params[:StartFrom] = start_from if start_from
|
|
18
|
+
params[:RetrieveCount] = retrieve_count if retrieve_count
|
|
19
|
+
|
|
20
|
+
@client.make_request('GET', 'events', nil, params)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def validate_events_params(start_date, end_date, event, retrieve_count)
|
|
26
|
+
if start_date && end_date && end_date < start_date
|
|
27
|
+
raise SendLayerValidationError.new("End date must be after start date")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if event && !VALID_EVENTS.include?(event)
|
|
31
|
+
raise SendLayerValidationError.new("Invalid event: #{event}. Valid events: #{VALID_EVENTS.join(', ')}")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
if retrieve_count && retrieve_count <= 0
|
|
35
|
+
raise SendLayerValidationError.new("RetrieveCount must be greater than 0")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module SendLayer
|
|
2
|
+
class SendLayerError < StandardError
|
|
3
|
+
attr_reader :message
|
|
4
|
+
|
|
5
|
+
def initialize(message)
|
|
6
|
+
@message = message
|
|
7
|
+
super(message)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class SendLayerAPIError < SendLayerError
|
|
12
|
+
attr_reader :status_code, :response
|
|
13
|
+
|
|
14
|
+
def initialize(message, status_code = nil, response = nil)
|
|
15
|
+
@status_code = status_code
|
|
16
|
+
@response = response
|
|
17
|
+
super(message)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class SendLayerAuthenticationError < SendLayerError; end
|
|
22
|
+
class SendLayerValidationError < SendLayerError; end
|
|
23
|
+
class SendLayerNotFoundError < SendLayerError; end
|
|
24
|
+
class SendLayerRateLimitError < SendLayerError; end
|
|
25
|
+
class SendLayerInternalServerError < SendLayerError; end
|
|
26
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module SendLayer
|
|
2
|
+
class Webhooks
|
|
3
|
+
VALID_EVENTS = %w[bounce click open unsubscribe complaint delivery].freeze
|
|
4
|
+
|
|
5
|
+
def initialize(client)
|
|
6
|
+
@client = client
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def create(url:, event:)
|
|
10
|
+
validate_webhook_params(url, event)
|
|
11
|
+
|
|
12
|
+
webhook_data = {
|
|
13
|
+
WebhookURL: url,
|
|
14
|
+
Event: event
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@client.make_request('POST', 'webhooks', webhook_data)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def get
|
|
21
|
+
@client.make_request('GET', 'webhooks')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def delete(webhook_id)
|
|
25
|
+
raise SendLayerValidationError.new("Webhook ID is required") if webhook_id.nil?
|
|
26
|
+
|
|
27
|
+
unless webhook_id.is_a?(Integer) && webhook_id > 0
|
|
28
|
+
raise SendLayerValidationError.new("Webhook ID must be a positive integer")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@client.make_request('DELETE', "webhooks/#{webhook_id}")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def validate_webhook_params(url, event)
|
|
37
|
+
raise SendLayerValidationError.new("URL is required") if url.nil? || url.empty?
|
|
38
|
+
raise SendLayerValidationError.new("Event is required") if event.nil? || event.empty?
|
|
39
|
+
|
|
40
|
+
unless VALID_EVENTS.include?(event)
|
|
41
|
+
raise SendLayerValidationError.new("Invalid event: #{event}. Valid event types include: #{VALID_EVENTS.join(', ')}")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Basic URL validation
|
|
45
|
+
uri = URI.parse(url)
|
|
46
|
+
unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
|
47
|
+
raise SendLayerValidationError.new("Invalid URL: #{url}")
|
|
48
|
+
end
|
|
49
|
+
rescue URI::InvalidURIError
|
|
50
|
+
raise SendLayerValidationError.new("Invalid URL: #{url}")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/sendlayer.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require_relative 'sendlayer/version'
|
|
2
|
+
require_relative 'sendlayer/client'
|
|
3
|
+
require_relative 'sendlayer/emails'
|
|
4
|
+
require_relative 'sendlayer/webhooks'
|
|
5
|
+
require_relative 'sendlayer/events'
|
|
6
|
+
require_relative 'sendlayer/exceptions'
|
|
7
|
+
|
|
8
|
+
module SendLayer
|
|
9
|
+
class SendLayer
|
|
10
|
+
attr_reader :emails, :webhooks, :events
|
|
11
|
+
|
|
12
|
+
def initialize(api_key, options = {})
|
|
13
|
+
@client = Client.new(api_key, options)
|
|
14
|
+
@emails = Emails.new(@client)
|
|
15
|
+
@webhooks = Webhooks.new(@client)
|
|
16
|
+
@events = Events.new(@client)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/sendlayer.gemspec
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/sendlayer/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "sendlayer"
|
|
7
|
+
spec.version = ::SendLayer::VERSION
|
|
8
|
+
spec.authors = ["SendLayer"]
|
|
9
|
+
spec.email = ["support@sendlayer.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Official Ruby SDK for SendLayer API"
|
|
12
|
+
spec.description = "The official Ruby SDK for interacting with the SendLayer API, providing a simple and intuitive interface for sending emails, managing webhooks, and retrieving email events."
|
|
13
|
+
spec.homepage = "https://github.com/sendlayer/sendlayer-ruby"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
spec.required_ruby_version = ">= 2.7.0"
|
|
16
|
+
|
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
|
+
spec.metadata["source_code_uri"] = "https://github.com/sendlayer/sendlayer-ruby"
|
|
19
|
+
spec.metadata["changelog_uri"] = "https://github.com/sendlayer/sendlayer-ruby/blob/main/CHANGELOG.md"
|
|
20
|
+
|
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
|
22
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
23
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
|
24
|
+
end
|
|
25
|
+
spec.bindir = "exe"
|
|
26
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
27
|
+
spec.require_paths = ["lib"]
|
|
28
|
+
|
|
29
|
+
# Dependencies
|
|
30
|
+
spec.add_dependency "mime-types", "~> 3.4"
|
|
31
|
+
|
|
32
|
+
# Development dependencies
|
|
33
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
|
34
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
35
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
|
36
|
+
spec.add_development_dependency "webmock", "~> 3.0"
|
|
37
|
+
spec.add_development_dependency "rubocop", "~> 1.0"
|
|
38
|
+
spec.add_development_dependency "simplecov", "~> 0.21"
|
|
39
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: sendlayer
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- SendLayer
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-10-09 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: mime-types
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '3.4'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '3.4'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: bundler
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '2.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rake
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '13.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '13.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rspec
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: webmock
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '3.0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '3.0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rubocop
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '1.0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '1.0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: simplecov
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '0.21'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0.21'
|
|
111
|
+
description: The official Ruby SDK for interacting with the SendLayer API, providing
|
|
112
|
+
a simple and intuitive interface for sending emails, managing webhooks, and retrieving
|
|
113
|
+
email events.
|
|
114
|
+
email:
|
|
115
|
+
- support@sendlayer.com
|
|
116
|
+
executables: []
|
|
117
|
+
extensions: []
|
|
118
|
+
extra_rdoc_files: []
|
|
119
|
+
files:
|
|
120
|
+
- ".env.example"
|
|
121
|
+
- ".github/workflows/publish.yml"
|
|
122
|
+
- ".gitignore"
|
|
123
|
+
- ".rubocop.yml"
|
|
124
|
+
- Gemfile
|
|
125
|
+
- Gemfile.lock
|
|
126
|
+
- LICENSE
|
|
127
|
+
- README.md
|
|
128
|
+
- Rakefile
|
|
129
|
+
- examples/events.rb
|
|
130
|
+
- examples/send_email.rb
|
|
131
|
+
- examples/webhooks.rb
|
|
132
|
+
- lib/sendlayer.rb
|
|
133
|
+
- lib/sendlayer/client.rb
|
|
134
|
+
- lib/sendlayer/emails.rb
|
|
135
|
+
- lib/sendlayer/events.rb
|
|
136
|
+
- lib/sendlayer/exceptions.rb
|
|
137
|
+
- lib/sendlayer/version.rb
|
|
138
|
+
- lib/sendlayer/webhooks.rb
|
|
139
|
+
- sendlayer.gemspec
|
|
140
|
+
homepage: https://github.com/sendlayer/sendlayer-ruby
|
|
141
|
+
licenses:
|
|
142
|
+
- MIT
|
|
143
|
+
metadata:
|
|
144
|
+
homepage_uri: https://github.com/sendlayer/sendlayer-ruby
|
|
145
|
+
source_code_uri: https://github.com/sendlayer/sendlayer-ruby
|
|
146
|
+
changelog_uri: https://github.com/sendlayer/sendlayer-ruby/blob/main/CHANGELOG.md
|
|
147
|
+
post_install_message:
|
|
148
|
+
rdoc_options: []
|
|
149
|
+
require_paths:
|
|
150
|
+
- lib
|
|
151
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
152
|
+
requirements:
|
|
153
|
+
- - ">="
|
|
154
|
+
- !ruby/object:Gem::Version
|
|
155
|
+
version: 2.7.0
|
|
156
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
157
|
+
requirements:
|
|
158
|
+
- - ">="
|
|
159
|
+
- !ruby/object:Gem::Version
|
|
160
|
+
version: '0'
|
|
161
|
+
requirements: []
|
|
162
|
+
rubygems_version: 3.4.19
|
|
163
|
+
signing_key:
|
|
164
|
+
specification_version: 4
|
|
165
|
+
summary: Official Ruby SDK for SendLayer API
|
|
166
|
+
test_files: []
|