mailersend-ruby 3.1.0 → 3.4.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +2 -0
  3. data/.github/workflows/main.yml +1 -1
  4. data/.github/workflows/publish_gem.yml +1 -1
  5. data/.gitignore +2 -1
  6. data/CHANGELOG.md +9 -0
  7. data/README.md +423 -0
  8. data/fixtures/email/email_send_template_language.yml +56 -0
  9. data/fixtures/emails/emails_list_emails.yml +35 -0
  10. data/fixtures/emails/emails_list_emails_empty_page.yml +35 -0
  11. data/fixtures/emails/emails_single_email.yml +35 -0
  12. data/fixtures/whatsapp/whatsapp_send.yml +47 -0
  13. data/fixtures/whatsapp/whatsapp_send_multiple_recipients.yml +47 -0
  14. data/fixtures/whatsapp/whatsapp_send_personalization.yml +47 -0
  15. data/fixtures/whatsapp_activities/whatsapp_activities_list.yml +43 -0
  16. data/fixtures/whatsapp_inbound_messages/whatsapp_inbound_messages_get.yml +43 -0
  17. data/fixtures/whatsapp_inbound_messages/whatsapp_inbound_messages_list.yml +43 -0
  18. data/fixtures/whatsapp_items/whatsapp_items_get.yml +43 -0
  19. data/fixtures/whatsapp_items/whatsapp_items_list.yml +43 -0
  20. data/fixtures/whatsapp_messages/whatsapp_messages_get.yml +43 -0
  21. data/fixtures/whatsapp_messages/whatsapp_messages_list.yml +43 -0
  22. data/fixtures/whatsapp_recipient/whatsapp_recipient_get.yml +43 -0
  23. data/fixtures/whatsapp_recipient/whatsapp_recipient_list.yml +43 -0
  24. data/fixtures/whatsapp_templates/whatsapp_templates_get.yml +43 -0
  25. data/fixtures/whatsapp_templates/whatsapp_templates_list.yml +43 -0
  26. data/flake.lock +27 -0
  27. data/flake.nix +25 -0
  28. data/lefthook.yml +14 -0
  29. data/lib/mailersend/email/email.rb +10 -0
  30. data/lib/mailersend/emails/emails.rb +44 -0
  31. data/lib/mailersend/version.rb +1 -1
  32. data/lib/mailersend/whatsapp/whatsapp.rb +47 -0
  33. data/lib/mailersend/whatsapp_activities/whatsapp_activities.rb +45 -0
  34. data/lib/mailersend/whatsapp_inbound_messages/whatsapp_inbound_messages.rb +47 -0
  35. data/lib/mailersend/whatsapp_items/whatsapp_items.rb +57 -0
  36. data/lib/mailersend/whatsapp_messages/whatsapp_messages.rb +47 -0
  37. data/lib/mailersend/whatsapp_recipient/whatsapp_recipient.rb +38 -0
  38. data/lib/mailersend/whatsapp_templates/whatsapp_templates.rb +44 -0
  39. data/lib/mailersend.rb +8 -0
  40. data/spec/email_rspec.rb +15 -0
  41. data/spec/emails_rspec.rb +175 -0
  42. data/spec/whatsapp_activities_rspec.rb +38 -0
  43. data/spec/whatsapp_inbound_messages_rspec.rb +49 -0
  44. data/spec/whatsapp_items_rspec.rb +52 -0
  45. data/spec/whatsapp_messages_rspec.rb +50 -0
  46. data/spec/whatsapp_recipient_rspec.rb +40 -0
  47. data/spec/whatsapp_rspec.rb +65 -0
  48. data/spec/whatsapp_templates_rspec.rb +48 -0
  49. metadata +39 -1
data/flake.nix ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ description = "MailerSend Ruby SDK environment";
3
+
4
+ inputs = {
5
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
6
+ };
7
+
8
+ outputs = { self, nixpkgs }:
9
+ let
10
+ systems = [ "aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux" ];
11
+ forEachSystem = f:
12
+ nixpkgs.lib.genAttrs systems (system: f system nixpkgs.legacyPackages.${system});
13
+ in
14
+ {
15
+ devShells = forEachSystem (system: pkgs: {
16
+ default = pkgs.mkShell {
17
+ packages = with pkgs; [
18
+ lefthook
19
+ gitleaks
20
+ git-secrets
21
+ ];
22
+ };
23
+ });
24
+ };
25
+ }
data/lefthook.yml ADDED
@@ -0,0 +1,14 @@
1
+ ---
2
+ pre-commit:
3
+ commands:
4
+ gitleaks:
5
+ run: gitleaks git --pre-commit --redact --staged --verbose --no-banner
6
+ fail_text: Staged changes contain a secret. Remove it; never commit and revert.
7
+ secrets:
8
+ run: |
9
+ git secrets --list | grep -q 'mssp' || {
10
+ git secrets --register-aws
11
+ git secrets --add 'mlsn\.[A-Za-z0-9._-]+'
12
+ git secrets --add 'mssp\.[A-Za-z0-9._-]+'
13
+ }
14
+ git secrets --scan {staged_files}
@@ -16,6 +16,7 @@ module Mailersend
16
16
  :reply_to,
17
17
  :attachments,
18
18
  :template_id,
19
+ :language,
19
20
  :tags,
20
21
  :variables,
21
22
  :personalization,
@@ -40,6 +41,7 @@ module Mailersend
40
41
  @headers = {}
41
42
  @list_unsubscribe = nil
42
43
  @send_at = send_at
44
+ @language = nil
43
45
  end
44
46
 
45
47
  def add_recipients(recipients)
@@ -86,6 +88,13 @@ module Mailersend
86
88
  @template_id = template_id
87
89
  end
88
90
 
91
+ # Language code (e.g. 'de', 'fr', 'pt-BR'). Only meaningful when a
92
+ # template_id is set; ignored for raw html/text sends. Max length 10.
93
+ # Supported: de, en, es, fr, it, lt, nl, pl, pt-BR
94
+ def add_language(language)
95
+ @language = language
96
+ end
97
+
89
98
  def add_tags(tags)
90
99
  @tags << tags
91
100
  end
@@ -131,6 +140,7 @@ module Mailersend
131
140
  'variables' => @variables,
132
141
  'personalization' => @personalization,
133
142
  'template_id' => @template_id,
143
+ 'language' => @language,
134
144
  'attachments' => @attachments,
135
145
  'headers' => @headers,
136
146
  'list_unsubscribe' => @list_unsubscribe,
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailersend
4
+ # Emails endpoint from MailerSend API.
5
+ class Emails
6
+ attr_accessor :client,
7
+ :domain_id,
8
+ :date_from,
9
+ :date_to,
10
+ :limit,
11
+ :page,
12
+ :email_id
13
+
14
+ def initialize(client = Mailersend::Client.new)
15
+ @client = client
16
+ end
17
+
18
+ def list(domain_id:, date_from:, date_to:, limit: nil, page: nil, status: nil, interaction: nil,
19
+ recipient_email: nil, message_id: nil, template_id: nil, subject: nil, tag: nil)
20
+ hash = {
21
+ 'domain_id' => domain_id,
22
+ 'date_from' => date_from,
23
+ 'date_to' => date_to,
24
+ 'limit' => limit,
25
+ 'page' => page,
26
+ # The API rejects a scalar status/interaction with 422, so they are always sent in array form.
27
+ 'status[]' => status,
28
+ 'interaction[]' => interaction,
29
+ 'recipient_email' => recipient_email,
30
+ 'message_id' => message_id,
31
+ 'template_id' => template_id,
32
+ 'subject' => subject,
33
+ 'tag' => tag
34
+ }
35
+
36
+ client.http.get(URI::HTTPS.build(host: MAILERSEND_API_BASE_HOST, path: '/v1/emails',
37
+ query: URI.encode_www_form(hash.compact)))
38
+ end
39
+
40
+ def single(email_id:)
41
+ client.http.get("#{MAILERSEND_API_URL}/email/#{email_id}")
42
+ end
43
+ end
44
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mailersend
4
- VERSION = '3.1.0'
4
+ VERSION = '3.4.0'
5
5
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailersend
4
+ # Send a WhatsApp message through MailerSend API
5
+ class WhatsApp
6
+ attr_accessor :client,
7
+ :from,
8
+ :to,
9
+ :template_id,
10
+ :personalization
11
+
12
+ def initialize(client = Mailersend::Client.new)
13
+ @client = client
14
+ @from = ''
15
+ @to = []
16
+ @template_id = ''
17
+ @personalization = []
18
+ end
19
+
20
+ def add_from(from)
21
+ @from = from
22
+ end
23
+
24
+ def add_to(to)
25
+ @to << to
26
+ end
27
+
28
+ def add_template_id(template_id)
29
+ @template_id = template_id
30
+ end
31
+
32
+ def add_personalization(personalization)
33
+ @personalization << personalization
34
+ end
35
+
36
+ def send
37
+ message = {
38
+ 'from' => @from,
39
+ 'to' => @to,
40
+ 'template_id' => @template_id,
41
+ 'personalization' => @personalization
42
+ }
43
+
44
+ client.http.post("#{MAILERSEND_API_URL}/whatsapp/send", json: message.delete_if { |_, value| value.to_s.strip == '' || value == [] || value == {} })
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailersend
4
+ # WhatsApp Activities endpoint from MailerSend API.
5
+ class WhatsAppActivities
6
+ attr_accessor :client,
7
+ :whatsapp_item_id,
8
+ :whatsapp_message_id,
9
+ :recipient,
10
+ :type,
11
+ :date_from,
12
+ :date_to,
13
+ :page,
14
+ :limit
15
+
16
+ def initialize(client = Mailersend::Client.new)
17
+ @client = client
18
+ @whatsapp_item_id = whatsapp_item_id
19
+ @whatsapp_message_id = whatsapp_message_id
20
+ @recipient = recipient
21
+ @type = type
22
+ @date_from = date_from
23
+ @date_to = date_to
24
+ @page = page
25
+ @limit = limit
26
+ end
27
+
28
+ def list(whatsapp_item_id: nil, whatsapp_message_id: nil, recipient: nil, type: nil, date_from: nil, date_to: nil,
29
+ page: nil, limit: nil)
30
+ hash = {
31
+ 'whatsapp_item_id' => whatsapp_item_id,
32
+ 'whatsapp_message_id' => whatsapp_message_id,
33
+ 'recipient' => recipient,
34
+ 'type[]' => type,
35
+ 'date_from' => date_from,
36
+ 'date_to' => date_to,
37
+ 'page' => page,
38
+ 'limit' => limit
39
+ }
40
+
41
+ client.http.get(URI::HTTPS.build(host: MAILERSEND_API_BASE_HOST, path: '/v1/whatsapp/activities',
42
+ query: URI.encode_www_form(hash.compact)))
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailersend
4
+ # WhatsApp Inbound Messages endpoint from MailerSend API.
5
+ class WhatsAppInboundMessages
6
+ attr_accessor :client,
7
+ :whatsapp_account_id,
8
+ :from,
9
+ :type,
10
+ :date_from,
11
+ :date_to,
12
+ :page,
13
+ :limit,
14
+ :whatsapp_inbound_message_id
15
+
16
+ def initialize(client = Mailersend::Client.new)
17
+ @client = client
18
+ @whatsapp_account_id = whatsapp_account_id
19
+ @from = from
20
+ @type = type
21
+ @date_from = date_from
22
+ @date_to = date_to
23
+ @page = page
24
+ @limit = limit
25
+ @whatsapp_inbound_message_id = whatsapp_inbound_message_id
26
+ end
27
+
28
+ def list(whatsapp_account_id: nil, from: nil, type: nil, date_from: nil, date_to: nil, page: nil, limit: nil)
29
+ hash = {
30
+ 'whatsapp_account_id' => whatsapp_account_id,
31
+ 'from' => from,
32
+ 'type[]' => type,
33
+ 'date_from' => date_from,
34
+ 'date_to' => date_to,
35
+ 'page' => page,
36
+ 'limit' => limit
37
+ }
38
+
39
+ client.http.get(URI::HTTPS.build(host: MAILERSEND_API_BASE_HOST, path: '/v1/whatsapp/inbound-messages',
40
+ query: URI.encode_www_form(hash.compact)))
41
+ end
42
+
43
+ def get(whatsapp_inbound_message_id:)
44
+ client.http.get("#{MAILERSEND_API_URL}/whatsapp/inbound-messages/#{whatsapp_inbound_message_id}")
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailersend
4
+ # WhatsApp Items endpoint from MailerSend API.
5
+ class WhatsAppItems
6
+ attr_accessor :client,
7
+ :recipient,
8
+ :whatsapp_message_id,
9
+ :status,
10
+ :whatsapp_account_id,
11
+ :from,
12
+ :template_id,
13
+ :date_from,
14
+ :date_to,
15
+ :page,
16
+ :limit,
17
+ :whatsapp_item_id
18
+
19
+ def initialize(client = Mailersend::Client.new)
20
+ @client = client
21
+ @recipient = recipient
22
+ @whatsapp_message_id = whatsapp_message_id
23
+ @status = status
24
+ @whatsapp_account_id = whatsapp_account_id
25
+ @from = from
26
+ @template_id = template_id
27
+ @date_from = date_from
28
+ @date_to = date_to
29
+ @page = page
30
+ @limit = limit
31
+ @whatsapp_item_id = whatsapp_item_id
32
+ end
33
+
34
+ def list(recipient: nil, whatsapp_message_id: nil, status: nil, whatsapp_account_id: nil, from: nil,
35
+ template_id: nil, date_from: nil, date_to: nil, page: nil, limit: nil)
36
+ hash = {
37
+ 'recipient' => recipient,
38
+ 'whatsapp_message_id' => whatsapp_message_id,
39
+ 'status[]' => status,
40
+ 'whatsapp_account_id' => whatsapp_account_id,
41
+ 'from' => from,
42
+ 'template_id' => template_id,
43
+ 'date_from' => date_from,
44
+ 'date_to' => date_to,
45
+ 'page' => page,
46
+ 'limit' => limit
47
+ }
48
+
49
+ client.http.get(URI::HTTPS.build(host: MAILERSEND_API_BASE_HOST, path: '/v1/whatsapp/items',
50
+ query: URI.encode_www_form(hash.compact)))
51
+ end
52
+
53
+ def get(whatsapp_item_id:)
54
+ client.http.get("#{MAILERSEND_API_URL}/whatsapp/items/#{whatsapp_item_id}")
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailersend
4
+ # WhatsApp Messages endpoint from MailerSend API.
5
+ class WhatsAppMessages
6
+ attr_accessor :client,
7
+ :whatsapp_account_id,
8
+ :from,
9
+ :template_id,
10
+ :date_from,
11
+ :date_to,
12
+ :page,
13
+ :limit,
14
+ :whatsapp_message_id
15
+
16
+ def initialize(client = Mailersend::Client.new)
17
+ @client = client
18
+ @whatsapp_account_id = whatsapp_account_id
19
+ @from = from
20
+ @template_id = template_id
21
+ @date_from = date_from
22
+ @date_to = date_to
23
+ @page = page
24
+ @limit = limit
25
+ @whatsapp_message_id = whatsapp_message_id
26
+ end
27
+
28
+ def list(whatsapp_account_id: nil, from: nil, template_id: nil, date_from: nil, date_to: nil, page: nil, limit: nil)
29
+ hash = {
30
+ 'whatsapp_account_id' => whatsapp_account_id,
31
+ 'from' => from,
32
+ 'template_id' => template_id,
33
+ 'date_from' => date_from,
34
+ 'date_to' => date_to,
35
+ 'page' => page,
36
+ 'limit' => limit
37
+ }
38
+
39
+ client.http.get(URI::HTTPS.build(host: MAILERSEND_API_BASE_HOST, path: '/v1/whatsapp/messages',
40
+ query: URI.encode_www_form(hash.compact)))
41
+ end
42
+
43
+ def get(whatsapp_message_id:)
44
+ client.http.get("#{MAILERSEND_API_URL}/whatsapp/messages/#{whatsapp_message_id}")
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailersend
4
+ # WhatsApp Recipient endpoint from MailerSend API.
5
+ class WhatsAppRecipient
6
+ attr_accessor :client,
7
+ :status,
8
+ :phone_number,
9
+ :page,
10
+ :limit,
11
+ :whatsapp_recipient_id
12
+
13
+ def initialize(client = Mailersend::Client.new)
14
+ @client = client
15
+ @status = status
16
+ @phone_number = phone_number
17
+ @page = page
18
+ @limit = limit
19
+ @whatsapp_recipient_id = whatsapp_recipient_id
20
+ end
21
+
22
+ def list(status: nil, phone_number: nil, page: nil, limit: nil)
23
+ hash = {
24
+ 'status' => status,
25
+ 'phone_number' => phone_number,
26
+ 'page' => page,
27
+ 'limit' => limit
28
+ }
29
+
30
+ client.http.get(URI::HTTPS.build(host: MAILERSEND_API_BASE_HOST, path: '/v1/whatsapp/recipients',
31
+ query: URI.encode_www_form(hash.compact)))
32
+ end
33
+
34
+ def get(whatsapp_recipient_id:)
35
+ client.http.get("#{MAILERSEND_API_URL}/whatsapp/recipients/#{whatsapp_recipient_id}")
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mailersend
4
+ # WhatsApp Templates endpoint from MailerSend API.
5
+ class WhatsAppTemplates
6
+ attr_accessor :client,
7
+ :whatsapp_account_id,
8
+ :status,
9
+ :category,
10
+ :language,
11
+ :page,
12
+ :limit,
13
+ :whatsapp_template_id
14
+
15
+ def initialize(client = Mailersend::Client.new)
16
+ @client = client
17
+ @whatsapp_account_id = whatsapp_account_id
18
+ @status = status
19
+ @category = category
20
+ @language = language
21
+ @page = page
22
+ @limit = limit
23
+ @whatsapp_template_id = whatsapp_template_id
24
+ end
25
+
26
+ def list(whatsapp_account_id: nil, status: nil, category: nil, language: nil, page: nil, limit: nil)
27
+ hash = {
28
+ 'whatsapp_account_id' => whatsapp_account_id,
29
+ 'status' => status,
30
+ 'category' => category,
31
+ 'language' => language,
32
+ 'page' => page,
33
+ 'limit' => limit
34
+ }
35
+
36
+ client.http.get(URI::HTTPS.build(host: MAILERSEND_API_BASE_HOST, path: '/v1/whatsapp/templates',
37
+ query: URI.encode_www_form(hash.compact)))
38
+ end
39
+
40
+ def get(whatsapp_template_id:)
41
+ client.http.get("#{MAILERSEND_API_URL}/whatsapp/templates/#{whatsapp_template_id}")
42
+ end
43
+ end
44
+ end
data/lib/mailersend.rb CHANGED
@@ -10,12 +10,20 @@ require_relative 'mailersend/dmarc_monitoring/dmarc_monitoring'
10
10
  require_relative 'mailersend/domains/domains'
11
11
  require_relative 'mailersend/email/email'
12
12
  require_relative 'mailersend/email_verification/email_verification'
13
+ require_relative 'mailersend/emails/emails'
13
14
  require_relative 'mailersend/inbound_routing/inbound_routing'
14
15
  require_relative 'mailersend/messages/messages'
15
16
  require_relative 'mailersend/recipients/recipients'
16
17
  require_relative 'mailersend/scheduled_messages/scheduled_messages'
17
18
  require_relative 'mailersend/sender_identity/sender_identity'
18
19
  require_relative 'mailersend/sms/sms'
20
+ require_relative 'mailersend/whatsapp/whatsapp'
21
+ require_relative 'mailersend/whatsapp_messages/whatsapp_messages'
22
+ require_relative 'mailersend/whatsapp_items/whatsapp_items'
23
+ require_relative 'mailersend/whatsapp_activities/whatsapp_activities'
24
+ require_relative 'mailersend/whatsapp_inbound_messages/whatsapp_inbound_messages'
25
+ require_relative 'mailersend/whatsapp_recipient/whatsapp_recipient'
26
+ require_relative 'mailersend/whatsapp_templates/whatsapp_templates'
19
27
  require_relative 'mailersend/sms_activity/sms_activity'
20
28
  require_relative 'mailersend/sms_messages/sms_messages'
21
29
  require_relative 'mailersend/sms_number/sms_number'
data/spec/email_rspec.rb CHANGED
@@ -26,4 +26,19 @@ RSpec.describe Mailersend::Email do
26
26
  expect(response.status).to eq(202)
27
27
  end
28
28
  end
29
+
30
+ it 'includes the language in the request body when set' do
31
+ # Cassette is matched on the request body, so this passes only when
32
+ # the serialized payload contains the language field.
33
+ VCR.use_cassette('email/email_send_template_language', match_requests_on: %i[method uri body]) do
34
+ email.from = { 'email' => 'sender@test-sdk.com', 'name' => 'Sender' }
35
+ email.recipients = [{ 'email' => 'test@mailerlite.com', 'name' => 'Test' }]
36
+ email.subject = 'Test Email'
37
+ email.add_template_id('123abc')
38
+ email.add_language('de')
39
+
40
+ response = email.send
41
+ expect(response.status).to eq(202)
42
+ end
43
+ end
29
44
  end
@@ -0,0 +1,175 @@
1
+ require 'rspec'
2
+ require 'vcr'
3
+ require 'json'
4
+
5
+ VCR.configure do |config|
6
+ config.cassette_library_dir = './fixtures'
7
+ config.hook_into :webmock
8
+ config.filter_sensitive_data('<AUTH>') do |interaction|
9
+ interaction.request.headers['Authorization'][0]
10
+ end
11
+ end
12
+
13
+ RSpec.describe Mailersend::Emails do
14
+ let(:client) { Mailersend::Client.new(API_TOKEN) }
15
+ let(:emails) { Mailersend::Emails.new(client) }
16
+
17
+ # The three required arguments, reused by every example.
18
+ let(:domain_id) { 'jpzkmgq7e5vl059v' }
19
+ let(:date_from) { 1_756_252_800 }
20
+ let(:date_to) { 1_756_339_200 }
21
+
22
+ describe '#list' do
23
+ # VCR matches on method + URI, so the `uri:` recorded in the cassette is itself the
24
+ # assertion that `list` hits `v1/emails` with domain_id, date_from, date_to, limit and page.
25
+ it 'returns a list of emails' do
26
+ VCR.use_cassette('emails/emails_list_emails', match_requests_on: %i[method uri]) do
27
+ response = emails.list(domain_id: domain_id, date_from: date_from, date_to: date_to, page: 1, limit: 10)
28
+ parsed_response = JSON.parse(response.body)
29
+
30
+ expect(response.status).to eq(200)
31
+ expect(parsed_response['data']).to be_an(Array)
32
+
33
+ email = parsed_response['data'].first
34
+
35
+ expect(email['id']).to eq('6a8fa9b1902fab56e0ce50dd')
36
+ expect(email['from']).to eq('sender@example.com')
37
+ expect(email['to']).to eq('rcpt@example.org')
38
+ expect(email['status']).to eq('sent')
39
+ expect(email['tags']).to eq(['newsletter'])
40
+ expect(email['interaction']).to eq(['opened'])
41
+ expect(email['suppression_reason']).to be_nil
42
+ expect(email['headers']).to eq([{ 'name' => 'X-Custom', 'value' => 'foo' }])
43
+
44
+ # List rows never carry content — it is only returned by `single`.
45
+ expect(email['text']).to be_nil
46
+ expect(email['html']).to be_nil
47
+
48
+ expect(parsed_response['links']['first']).to eq('https://api.mailersend.com/v1/emails?page=1')
49
+ expect(parsed_response['links']['last']).to be_nil
50
+ expect(parsed_response['meta']['current_page']).to eq(1)
51
+ expect(parsed_response['meta']['per_page']).to eq(10)
52
+ end
53
+ end
54
+
55
+ it 'returns an empty page past the end of the result set' do
56
+ VCR.use_cassette('emails/emails_list_emails_empty_page', match_requests_on: %i[method uri]) do
57
+ response = emails.list(domain_id: domain_id, date_from: date_from, date_to: date_to, page: 2, limit: 10)
58
+ parsed_response = JSON.parse(response.body)
59
+
60
+ expect(response.status).to eq(200)
61
+ expect(parsed_response['data']).to eq([])
62
+ expect(parsed_response['links']['prev']).to eq('https://api.mailersend.com/v1/emails?page=1')
63
+ expect(parsed_response['links']['next']).to be_nil
64
+ expect(parsed_response['meta']['current_page']).to eq(2)
65
+ expect(parsed_response['meta']['from']).to be_nil
66
+ expect(parsed_response['meta']['to']).to be_nil
67
+ end
68
+ end
69
+ end
70
+
71
+ describe '#single' do
72
+ # The path is singular (`v1/email/{id}`) while `list` is plural (`v1/emails`); the cassette
73
+ # URI pins that difference.
74
+ it 'returns a single email with its recipient and activity' do
75
+ VCR.use_cassette('emails/emails_single_email', match_requests_on: %i[method uri]) do
76
+ response = emails.single(email_id: '6a8fa9b1902fab56e0ce50dd')
77
+ parsed_response = JSON.parse(response.body)
78
+
79
+ expect(response.status).to eq(200)
80
+ expect(parsed_response['data']).to be_a(Hash)
81
+ expect(parsed_response['data']['id']).to eq('6a8fa9b1902fab56e0ce50dd')
82
+
83
+ # `single` adds content, the recipient object and the activity array on top of a list row.
84
+ expect(parsed_response['data']['text']).to eq('Welcome aboard.')
85
+ expect(parsed_response['data']['html']).to eq('<p>Welcome aboard.</p>')
86
+ expect(parsed_response['data']['recipient']).to be_a(Hash)
87
+ expect(parsed_response['data']['recipient']['email']).to eq('rcpt@example.org')
88
+
89
+ activity = parsed_response['data']['activity']
90
+
91
+ expect(activity).to be_an(Array)
92
+ expect(activity.length).to eq(3)
93
+ expect(activity.first).to include('id', 'type', 'created_at')
94
+ expect(activity.first['type']).to eq('opened')
95
+ expect(activity.map { |event| event['type'] }).to eq(%w[opened delivered sent])
96
+ end
97
+ end
98
+ end
99
+
100
+ # These examples assert the URI the SDK builds rather than replaying a cassette, because query
101
+ # serialization is the contract that matters most here: the API rejects a scalar `status` or
102
+ # `interaction` with 422, so both must go out as repeated `status[]` / `interaction[]` params and
103
+ # never as a single comma-joined value.
104
+ describe 'query serialization' do
105
+ let(:http) { double('http') }
106
+ let(:requested_urls) { [] }
107
+
108
+ before do
109
+ allow(client).to receive(:http).and_return(http)
110
+ allow(http).to receive(:get) { |url| requested_urls << url.to_s }
111
+ end
112
+
113
+ it 'sends status and interaction as repeated array params' do
114
+ emails.list(domain_id: domain_id, date_from: date_from, date_to: date_to, limit: 10, page: 1,
115
+ status: %w[sent delivered], interaction: %w[opened clicked])
116
+
117
+ expect(requested_urls.last).to eq(
118
+ 'https://api.mailersend.com/v1/emails' \
119
+ '?domain_id=jpzkmgq7e5vl059v&date_from=1756252800&date_to=1756339200&limit=10&page=1' \
120
+ '&status%5B%5D=sent&status%5B%5D=delivered' \
121
+ '&interaction%5B%5D=opened&interaction%5B%5D=clicked'
122
+ )
123
+
124
+ # One key per value, and no comma-joined value anywhere (%2C is an encoded comma).
125
+ expect(requested_urls.last.scan('status%5B%5D=').length).to eq(2)
126
+ expect(requested_urls.last.scan('interaction%5B%5D=').length).to eq(2)
127
+ expect(requested_urls.last).not_to include('%2C')
128
+ end
129
+
130
+ it 'sends a scalar status and interaction in array form too' do
131
+ emails.list(domain_id: domain_id, date_from: date_from, date_to: date_to,
132
+ status: 'sent', interaction: 'opened')
133
+
134
+ expect(requested_urls.last).to eq(
135
+ 'https://api.mailersend.com/v1/emails' \
136
+ '?domain_id=jpzkmgq7e5vl059v&date_from=1756252800&date_to=1756339200' \
137
+ '&status%5B%5D=sent&interaction%5B%5D=opened'
138
+ )
139
+ end
140
+
141
+ it 'includes every optional filter when set' do
142
+ emails.list(domain_id: domain_id, date_from: date_from, date_to: date_to, limit: 25, page: 2,
143
+ status: %w[sent], interaction: %w[opened], recipient_email: 'rcpt@example.org',
144
+ message_id: '6a8fa9b1902fab56e0ce50aa', template_id: '7nxe3yjmeq28vp0k',
145
+ subject: 'Welcome', tag: 'newsletter')
146
+
147
+ expect(requested_urls.last).to eq(
148
+ 'https://api.mailersend.com/v1/emails' \
149
+ '?domain_id=jpzkmgq7e5vl059v&date_from=1756252800&date_to=1756339200&limit=25&page=2' \
150
+ '&status%5B%5D=sent&interaction%5B%5D=opened' \
151
+ '&recipient_email=rcpt%40example.org&message_id=6a8fa9b1902fab56e0ce50aa' \
152
+ '&template_id=7nxe3yjmeq28vp0k&subject=Welcome&tag=newsletter'
153
+ )
154
+ end
155
+
156
+ it 'omits every optional filter that is nil' do
157
+ emails.list(domain_id: domain_id, date_from: date_from, date_to: date_to)
158
+
159
+ expect(requested_urls.last).to eq(
160
+ 'https://api.mailersend.com/v1/emails?domain_id=jpzkmgq7e5vl059v&date_from=1756252800&date_to=1756339200'
161
+ )
162
+
163
+ %w[limit page status interaction recipient_email message_id template_id subject tag].each do |param|
164
+ expect(requested_urls.last).not_to include(param)
165
+ end
166
+ end
167
+
168
+ it 'requests the singular v1/email/{id} path for a single email' do
169
+ emails.single(email_id: '6a8fa9b1902fab56e0ce50dd')
170
+
171
+ expect(requested_urls.last).to eq('https://api.mailersend.com/v1/email/6a8fa9b1902fab56e0ce50dd')
172
+ expect(requested_urls.last).not_to include('/v1/emails/')
173
+ end
174
+ end
175
+ end