From 96d69a254288562d1867fd4fd54e1b8cf2056407 Mon Sep 17 00:00:00 2001 From: Majd Sehwail Date: Tue, 18 Feb 2025 13:16:09 +0200 Subject: [PATCH 1/4] Implement SES V2 API functions for create/get/put/delete email identity --- lib/ex_aws/ses.ex | 137 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 132 insertions(+), 5 deletions(-) diff --git a/lib/ex_aws/ses.ex b/lib/ex_aws/ses.ex index 79f91f7..74fa8df 100644 --- a/lib/ex_aws/ses.ex +++ b/lib/ex_aws/ses.ex @@ -11,6 +11,134 @@ defmodule ExAws.SES do @service :ses @v2_path "/v2/email" + @type create_email_identity_opt :: + {:configuration_set_name, String.t()} + | {:dkim_signing_attributes, + %{ + DomainSigningAttributesOrigin: String.t(), + DomainSigningPrivateKey: String.t(), + DomainSigningSelector: String.t(), + NextSigningKeyLength: String.t() + }} + | {:tags, %{(String.t() | atom) => String.t()}} + + @doc """ + Create an Email identity list via the SES V2 API. + See https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_CreateEmailIdentity.html. + + ## Examples + + ExAws.SES.create_email_identity( + "mydomain.com", + [ + configuration_set_name: "my_configuration_set", + dkim_signing_attributes: %{ + DomainSigningAttributesOrigin: "AWS_SES", + DomainSigningPrivateKey: "my_private_key", + DomainSigningSelector: "my_selector", + NextSigningKeyLength: "2048" + }, + tags: [%{"Key" => "environment", "Value" => "test"}] + ] + ) + + """ + @spec create_email_identity(identity :: binary, opts :: [] | [create_email_identity_opt]) :: ExAws.Operation.JSON.t() + def create_email_identity(identity, opts \\ []) do + data = + prune_map(%{ + "EmailIdentity" => identity, + "ConfigurationSetName" => opts[:configuration_set_name], + "DkimSigningAttributes" => opts[:dkim_signing_attributes], + "Tags" => opts[:tags] + }) + + :post + |> request_v2("identities") + |> Map.put(:data, data) + end + + @doc """ + Provides information about a specific identity via the SES V2 API. + See https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_GetEmailIdentity.html + + ## Examples + + ExAws.SES.get_email_identity("mydomain.com") + """ + @spec get_email_identity(identity :: binary) :: ExAws.Operation.JSON.t() + def get_email_identity(identity) do + encoded_identity = ExAws.Request.Url.uri_encode(identity) + + request_v2(:get, "identities/#{encoded_identity}") + end + + @doc """ + Used to associate a configuration set with an email identity. + See https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_PutEmailIdentityConfigurationSetAttributes.html + """ + + @spec put_email_identity_configuration_set_attributes(identity :: binary, configuration_set_name :: binary) :: + ExAws.Operation.JSON.t() + def put_email_identity_configuration_set_attributes(identity, configuration_set_name) do + encoded_identity = ExAws.Request.Url.uri_encode(identity) + + data = prune_map(%{"ConfigurationSetName" => configuration_set_name}) + + request_v2(:put, "identities/#{encoded_identity}/configuration-set") + |> Map.put(:data, data) + end + + @type put_email_identity_mail_from_attributes_opt :: + {:BehaviorOnMxFailure, String.t()} | {:MailFromDomain, String.t()} + + @doc """ + Used to enable or disable the custom Mail-From domain configuration for an email identity via the SES V2 API. + See https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_PutEmailIdentityMailFromAttributes.html + + ## Examples + + ExAws.SES.put_email_identity_mail_from_attributes( + "mydomain.com", + [ + BehaviorOnMxFailure: "USE_DEFAULT_VALUE" || "REJECT_MESSAGE", + MailFromDomain: "subdomain.mydomain.com" + ] + ) + """ + @spec put_email_identity_mail_from_attributes( + identity :: binary, + opts :: [put_email_identity_mail_from_attributes_opt] + ) :: ExAws.Operation.JSON.t() + def put_email_identity_mail_from_attributes(identity, opts) do + encoded_identity = ExAws.Request.Url.uri_encode(identity) + + data = + prune_map(%{ + "BehaviorOnMxFailure" => opts[:BehaviorOnMxFailure], + "MailFromDomain" => opts[:MailFromDomain] + }) + + :put + |> request_v2("identities/#{encoded_identity}/mail-from") + |> Map.put(:data, data) + end + + @doc """ + Deletes an email identity via the SES V2 API. + See https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_DeleteEmailIdentity.html + + ## Examples + + ExAws.SES.delete_email_identity("mydomain.com") + """ + @spec delete_email_identity(identity :: binary) :: ExAws.Operation.JSON.t() + def delete_email_identity(identity) do + encoded_identity = ExAws.Request.Url.uri_encode(identity) + + request_v2(:delete, "identities/#{encoded_identity}") + end + @doc """ Verifies an email address. """ @@ -387,11 +515,10 @@ defmodule ExAws.SES do |> put_if_not_nil("Html", html) |> put_if_not_nil("Text", text) - params = - %{ - "TemplateName" => template_name, - "TemplateContent" => template_content - } + params = %{ + "TemplateName" => template_name, + "TemplateContent" => template_content + } request_v2(:post, "templates") |> Map.put(:data, params) From 91c919201211e355257d6d3bd938cb12d4c9aef7 Mon Sep 17 00:00:00 2001 From: Majd Sehwail Date: Wed, 19 Feb 2025 18:06:02 +0200 Subject: [PATCH 2/4] Fix tags opt in `create_email_identity/2` --- lib/ex_aws/ses.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/ex_aws/ses.ex b/lib/ex_aws/ses.ex index 74fa8df..e8f93e5 100644 --- a/lib/ex_aws/ses.ex +++ b/lib/ex_aws/ses.ex @@ -50,7 +50,7 @@ defmodule ExAws.SES do "EmailIdentity" => identity, "ConfigurationSetName" => opts[:configuration_set_name], "DkimSigningAttributes" => opts[:dkim_signing_attributes], - "Tags" => opts[:tags] + "Tags" => format_tags_v2(opts[:tags]) }) :post @@ -1063,6 +1063,9 @@ defmodule ExAws.SES do end) end + defp format_tags_v2(tags), + do: Enum.map(tags, fn {k, v} -> %{"Key" => to_string(k), "Value" => to_string(v)} end) + ## Request ###################### From 8376056daa1d2635f08fa4c0055203b02b956e5b Mon Sep 17 00:00:00 2001 From: Majd Sehwail Date: Sat, 6 Sep 2025 15:50:32 +0300 Subject: [PATCH 3/4] Support list email identities SES API V2 + run mix format --- lib/ex_aws/ses.ex | 15 ++++++++++ lib/ex_aws/ses/parsers.ex | 58 +++++++++++++++++++----------------- test/lib/ses/parser_test.exs | 30 ++++++++++++++----- 3 files changed, 68 insertions(+), 35 deletions(-) diff --git a/lib/ex_aws/ses.ex b/lib/ex_aws/ses.ex index e8f93e5..18de3e4 100644 --- a/lib/ex_aws/ses.ex +++ b/lib/ex_aws/ses.ex @@ -73,6 +73,21 @@ defmodule ExAws.SES do request_v2(:get, "identities/#{encoded_identity}") end + @type list_email_identities_opt :: {:page_size, pos_integer} | {:next_token, String.t()} + @doc """ + List email identities using the SES V2 API. + See https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_ListEmailIdentities.html + + ## Examples + + ExAws.SES.list_email_identities([page_size: 100, next_token: nil]) + """ + @spec list_email_identities(opts :: [] | [list_email_identities_opt]) :: ExAws.Operation.JSON.t() + def list_email_identities(opts \\ []) do + params = build_opts(opts, [:page_size, :next_token]) + request_v2(:get, "identities?#{URI.encode_query(params)}") + end + @doc """ Used to associate a configuration set with an email identity. See https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_PutEmailIdentityConfigurationSetAttributes.html diff --git a/lib/ex_aws/ses/parsers.ex b/lib/ex_aws/ses/parsers.ex index f01185f..2e8243e 100644 --- a/lib/ex_aws/ses/parsers.ex +++ b/lib/ex_aws/ses/parsers.ex @@ -11,27 +11,28 @@ if Code.ensure_loaded?(SweetXml) do end def parse({:ok, %{body: xml} = resp}, :verify_domain_identity) do - parsed_body = SweetXml.xpath(xml, ~x"//VerifyDomainIdentityResponse", - verification_token: ~x"./VerifyDomainIdentityResult/VerificationToken/text()"s, - request_id: request_id_xpath() - ) + parsed_body = + SweetXml.xpath(xml, ~x"//VerifyDomainIdentityResponse", + verification_token: ~x"./VerifyDomainIdentityResult/VerificationToken/text()"s, + request_id: request_id_xpath() + ) {:ok, Map.put(resp, :body, parsed_body)} end def parse({:ok, %{body: xml} = resp}, :verify_domain_dkim) do - parsed_body = SweetXml.xpath(xml, ~x"//VerifyDomainDkimResponse", - dkim_tokens: [ - ~x"./VerifyDomainDkimResult", - members: ~x"./DkimTokens/member/text()"ls - ], - request_id: request_id_xpath() - ) + parsed_body = + SweetXml.xpath(xml, ~x"//VerifyDomainDkimResponse", + dkim_tokens: [ + ~x"./VerifyDomainDkimResult", + members: ~x"./DkimTokens/member/text()"ls + ], + request_id: request_id_xpath() + ) {:ok, Map.put(resp, :body, parsed_body)} end - def parse({:ok, %{body: xml} = resp}, :get_identity_verification_attributes) do parsed_body = xml @@ -57,7 +58,9 @@ if Code.ensure_loaded?(SweetXml) do ~x"./ListIdentitiesResult", members: ~x"./Identities/member/text()"ls, next_token: ~x"./NextToken/text()"so - ], #TODO: Remove this key in the next major version, 3.x.x + ], + + # TODO: Remove this key in the next major version, 3.x.x identities: [ ~x"./ListIdentitiesResult", members: ~x"./Identities/member/text()"ls, @@ -201,20 +204,21 @@ if Code.ensure_loaded?(SweetXml) do end def parse({:ok, %{body: xml} = resp}, :describe_receipt_rule_set) do - parsed_body = SweetXml.xpath(xml, ~x"//DescribeReceiptRuleSetResponse", - rules: [ - ~x"./DescribeReceiptRuleSetResult", - members: [ - ~x"./Rules/member"l, - enabled: ~x"./Enabled/text()"so |> transform_to_boolean(), - name: ~x"./Name/text()"s, - recipients: ~x"./Recipients/member/text()"ls, - scan_enabled: ~x"./ScanEnabled/text()"so |> transform_to_boolean(), - tls_policy: ~x"./TlsPolicy/text()"so, - ], - ], - request_id: request_id_xpath() - ) + parsed_body = + SweetXml.xpath(xml, ~x"//DescribeReceiptRuleSetResponse", + rules: [ + ~x"./DescribeReceiptRuleSetResult", + members: [ + ~x"./Rules/member"l, + enabled: ~x"./Enabled/text()"so |> transform_to_boolean(), + name: ~x"./Name/text()"s, + recipients: ~x"./Recipients/member/text()"ls, + scan_enabled: ~x"./ScanEnabled/text()"so |> transform_to_boolean(), + tls_policy: ~x"./TlsPolicy/text()"so + ] + ], + request_id: request_id_xpath() + ) {:ok, Map.put(resp, :body, parsed_body)} end diff --git a/test/lib/ses/parser_test.exs b/test/lib/ses/parser_test.exs index 82b048c..3bb2b2c 100644 --- a/test/lib/ses/parser_test.exs +++ b/test/lib/ses/parser_test.exs @@ -42,7 +42,11 @@ defmodule ExAws.SES.ParserTest do |> to_success {:ok, %{body: parsed_doc}} = Parsers.parse(rsp, :verify_domain_identity) - assert parsed_doc == %{request_id: "d8eb8250-be9b-11e6-b7f7-d570946af758", verification_token: "u4GmlJ3cPJfxxZbLSPMkLOPjQvJW1HPvA6Pmi21CPIE="} + + assert parsed_doc == %{ + request_id: "d8eb8250-be9b-11e6-b7f7-d570946af758", + verification_token: "u4GmlJ3cPJfxxZbLSPMkLOPjQvJW1HPvA6Pmi21CPIE=" + } end test "#parse a verify_domain_dkim response" do @@ -64,9 +68,18 @@ defmodule ExAws.SES.ParserTest do |> to_success {:ok, %{body: parsed_doc}} = Parsers.parse(rsp, :verify_domain_dkim) - assert parsed_doc == %{request_id: "d8eb8250-be9b-11e6-b7f7-d570946af758", dkim_tokens: %{members: ["5livxhounddpfqprdog22m4c337ake5o", "tbnwx5g3l0zmstwf2c258r36pvpnksbt", "bbtl43drumsloilm2zfjlhj3c7v12a5d"]}} - end + assert parsed_doc == %{ + request_id: "d8eb8250-be9b-11e6-b7f7-d570946af758", + dkim_tokens: %{ + members: [ + "5livxhounddpfqprdog22m4c337ake5o", + "tbnwx5g3l0zmstwf2c258r36pvpnksbt", + "bbtl43drumsloilm2zfjlhj3c7v12a5d" + ] + } + } + end test "#parse identity_verification_attributes" do rsp = @@ -230,11 +243,12 @@ defmodule ExAws.SES.ParserTest do """ |> to_success() - {:ok, %{body: parsed_doc}} = Parsers.parse(rsp, :send_raw_email) - assert parsed_doc == %{ - request_id: "3c2ddfd4-ff21-4a3d-af5f-97ec74811e22", - message_id: "0101018264278cea-406a0406-5f46-412b-8f32-05ef31a62aa0-000000" - } + {:ok, %{body: parsed_doc}} = Parsers.parse(rsp, :send_raw_email) + + assert parsed_doc == %{ + request_id: "3c2ddfd4-ff21-4a3d-af5f-97ec74811e22", + message_id: "0101018264278cea-406a0406-5f46-412b-8f32-05ef31a62aa0-000000" + } end test "#parse a delete_identity response" do From 2db120eb4b4fa7ea7f63bcf1aeb821bb905793e0 Mon Sep 17 00:00:00 2001 From: Majd Sehwail Date: Sun, 28 Sep 2025 13:00:21 +0300 Subject: [PATCH 4/4] Support get configuration set SES API V2 --- lib/ex_aws/ses.ex | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/ex_aws/ses.ex b/lib/ex_aws/ses.ex index 18de3e4..9a96235 100644 --- a/lib/ex_aws/ses.ex +++ b/lib/ex_aws/ses.ex @@ -204,6 +204,20 @@ defmodule ExAws.SES do request(:get_identity_verification_attributes, params) end + ## Configuration sets + ###################### + + @doc """ + Fetch configuration set from configuration set name via the SES V2 API. + See https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_GetConfigurationSet.html + """ + @spec get_configuration_set(identity :: binary) :: ExAws.Operation.JSON.t() + def get_configuration_set(configuration_set_name) do + encoded_configuration_set_name = ExAws.Request.Url.uri_encode(configuration_set_name) + + request_v2(:get, "configuration-sets/#{encoded_configuration_set_name}") + end + @type list_configuration_sets_opt :: {:max_items, pos_integer} | {:next_token, String.t()}