├── .formatter.exs ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── README.md ├── config └── config.exs ├── lib └── ex_aws │ ├── ses.ex │ └── ses │ └── parsers.ex ├── mix.exs ├── mix.lock └── test ├── lib ├── ses │ └── parser_test.exs └── ses_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # src https://gist.github.com/dberget/f4d157603a90cda95f289c06858dd04c 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | locals_without_parens: [ 5 | # Kernel 6 | inspect: 1, 7 | inspect: 2, 8 | 9 | # Tests 10 | assert: 1, 11 | assert: 2, 12 | assert: 3, 13 | on_exit: 1 14 | ], 15 | line_length: 120 16 | ] 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | ex_aws_ses-*.tar 24 | 25 | # Temporary files for e.g. tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## v2.4.1 - 2021-03-03 11 | 12 | - Fix email address encoding in `PutSuppressedDestination` `DeleteSuppressedDestination` by @mtarnovan 13 | - Switch to from Poison to Jason 14 | 15 | ## v2.4.0 - 2022-03-21 16 | 17 | - Add v2 API's [PutSuppressedDestination](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_PutSuppressedDestination.html) by @mtarnovan 18 | - Add v2 API's [DeleteSuppressedDestination](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_DeleteSuppressedDestination.html) by @mtarnovan 19 | 20 | ## v2.3.0 - 2021-05-02 21 | 22 | - Add functions for custom verification emails by @wmnnd 23 | 24 | ## v2.2.0 - 2021-04-23 25 | 26 | - Add CRUD actions on contact list and contact resources by j4p3 27 | - Add v2 API's [SendEmail](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_SendEmail.html) action by j4p3 28 | 29 | ## v2.1.1 - 2019-09-12 30 | 31 | - Added support of `ReplyToAddresses.member.N` option to [SendBulkTemplatedEmail](https://docs.aws.amazon.com/ses/latest/APIReference/API_SendBulkTemplatedEmail.html) action by @flyrboy96 32 | - Spec improvements by @flyrboy96 33 | 34 | ## v2.1.0 - 2019-03-09 35 | 36 | - Added support of [CreateTemplate](https://docs.aws.amazon.com/ses/latest/APIReference/API_CreateTemplate.html) action by @themerch 37 | - Added support of [DeleteTemplate](https://docs.aws.amazon.com/ses/latest/APIReference/API_DeleteTemplate.html) action by @themerch 38 | - Added support of [SendBulkTemplatedEmail](https://docs.aws.amazon.com/ses/latest/APIReference/API_SendBulkTemplatedEmail.html) action by @themerch 39 | 40 | ## v2.0.2 - 2018-08-08 41 | 42 | - Added support for [SendTemplatedEmail](https://docs.aws.amazon.com/ses/latest/APIReference/API_SendTemplatedEmail.html) action with exception for two optional parameters: `TemplateArn` and `ReplyToAddresses.member.N`. by @xfumihiro 43 | - Fixed wrong key in destination typespec by @kalys 44 | - Fixed broken typespec contracts 45 | 46 | ## v2.0.1 - 2018-06-27 47 | 48 | - Improved Mix configuration 49 | 50 | ## v2.0.0 - 2017-11-10 51 | 52 | - Major Project Split. Please see the main ExAws repository for previous changelogs. 53 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Contributions to ExAws are always welcome! For contributions to this particular service, feel free to just open a PR or an issue. For larger scale contributions see: https://github.com/ex-aws/ex_aws/blob/master/CONTRIBUTING.md 5 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * Do not use the issues tracker for help or support (try Elixir Forum, Slack, IRC, etc.) 2 | * Questions about how to contribute are fine. 3 | 4 | ### Environment 5 | 6 | * Elixir & Erlang versions (elixir --version): 7 | * ExAws version `mix deps |grep ex_aws` 8 | * HTTP client version. IE for hackney do `mix deps | grep hackney` 9 | 10 | ### Current behavior 11 | 12 | Include code samples, errors and stacktraces if appropriate. 13 | 14 | ### Expected behavior 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExAws.SES 2 | 3 | [![Module Version](https://img.shields.io/hexpm/v/ex_aws_ses.svg)](https://hex.pm/packages/ex_aws_ses) 4 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/ex_aws_ses/) 5 | [![Total Download](https://img.shields.io/hexpm/dt/ex_aws_ses.svg)](https://hex.pm/packages/ex_aws_ses) 6 | [![License](https://img.shields.io/hexpm/l/ex_aws_ses.svg)](https://github.com/ex-aws/ex_aws_ses/blob/master/LICENSE) 7 | [![Last Updated](https://img.shields.io/github/last-commit/ex-aws/ex_aws_ses.svg)](https://github.com/ex-aws/ex_aws_ses/commits/master) 8 | 9 | Service module for [https://github.com/ex-aws/ex_aws](https://github.com/ex-aws/ex_aws). 10 | 11 | ## Installation 12 | 13 | The package can be installed by adding `:ex_aws_ses` to your list of dependencies in `mix.exs` 14 | along with `:ex_aws` and your preferred JSON codec / HTTP client 15 | 16 | ```elixir 17 | def deps do 18 | [ 19 | {:ex_aws, "~> 2.0"}, 20 | {:ex_aws_ses, "~> 2.0"}, 21 | {:poison, "~> 3.0"}, 22 | {:hackney, "~> 1.9"}, 23 | ] 24 | end 25 | ``` 26 | 27 | Documentation can be found at [https://hexdocs.pm/ex_aws_ses](https://hexdocs.pm/ex_aws_ses). 28 | 29 | ## License 30 | 31 | The MIT License (MIT) 32 | 33 | Copyright (c) 2014 CargoSense, Inc. 34 | 35 | Permission is hereby granted, free of charge, to any person obtaining a copy 36 | of this software and associated documentation files (the "Software"), to deal 37 | in the Software without restriction, including without limitation the rights 38 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 39 | copies of the Software, and to permit persons to whom the Software is 40 | furnished to do so, subject to the following conditions: 41 | 42 | The above copyright notice and this permission notice shall be included in 43 | all copies or substantial portions of the Software. 44 | 45 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 46 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 47 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 48 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 49 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 50 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 51 | THE SOFTWARE. 52 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | import Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :ex_aws_ses, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:ex_aws_ses, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /lib/ex_aws/ses.ex: -------------------------------------------------------------------------------- 1 | defmodule ExAws.SES do 2 | import ExAws.Utils, only: [camelize_key: 1, camelize_keys: 1] 3 | 4 | @moduledoc """ 5 | Operations on AWS SES. 6 | 7 | See https://docs.aws.amazon.com/ses/latest/APIReference/Welcome.html 8 | """ 9 | 10 | @notification_types [:bounce, :complaint, :delivery] 11 | @service :ses 12 | @v2_path "/v2/email" 13 | 14 | @doc """ 15 | Verifies an email address. 16 | """ 17 | @spec verify_email_identity(email :: binary) :: ExAws.Operation.Query.t() 18 | def verify_email_identity(email) do 19 | request(:verify_email_identity, %{"EmailAddress" => email}) 20 | end 21 | 22 | @doc """ 23 | Verifies a domain. 24 | """ 25 | @spec verify_domain_identity(domain :: binary) :: ExAws.Operation.Query.t() 26 | def verify_domain_identity(domain) do 27 | request(:verify_domain_identity, %{"Domain" => domain}) 28 | end 29 | 30 | @doc """ 31 | Verifies a domain with DKIM. 32 | """ 33 | @spec verify_domain_dkim(domain :: binary) :: ExAws.Operation.Query.t() 34 | def verify_domain_dkim(domain) do 35 | request(:verify_domain_dkim, %{"Domain" => domain}) 36 | end 37 | 38 | @type list_identities_opt :: 39 | {:max_items, pos_integer} 40 | | {:next_token, String.t()} 41 | | {:identity_type, String.t()} 42 | 43 | @type tag :: %{Key: String.t(), Value: String.t()} 44 | @type list_topic :: %{String.t() => String.t()} 45 | @type suppression_reason :: :BOUNCE | :COMPLAINT 46 | 47 | @doc "List identities associated with the AWS account" 48 | @spec list_identities(opts :: [] | [list_identities_opt]) :: ExAws.Operation.Query.t() 49 | @deprecated "The :custom_verification_templates key will be deprecated in version 3.x.x, please use :identities instead" 50 | def list_identities(opts \\ []) do 51 | params = build_opts(opts, [:max_items, :next_token, :identity_type]) 52 | request(:list_identities, params) 53 | end 54 | 55 | @doc """ 56 | Fetch identities verification status and token (for domains). 57 | """ 58 | @spec get_identity_verification_attributes([binary]) :: ExAws.Operation.Query.t() 59 | def get_identity_verification_attributes(identities) when is_list(identities) do 60 | params = format_member_attribute({:identities, identities}) 61 | request(:get_identity_verification_attributes, params) 62 | end 63 | 64 | @type list_configuration_sets_opt :: 65 | {:max_items, pos_integer} 66 | | {:next_token, String.t()} 67 | 68 | @doc """ 69 | Fetch configuration sets associated with AWS account. 70 | """ 71 | @spec list_configuration_sets() :: ExAws.Operation.Query.t() 72 | @spec list_configuration_sets(opts :: [] | [list_configuration_sets_opt]) :: ExAws.Operation.Query.t() 73 | def list_configuration_sets(opts \\ []) do 74 | params = build_opts(opts, [:max_items, :next_token]) 75 | request(:list_configuration_sets, params) 76 | end 77 | 78 | ## Contact lists 79 | ###################### 80 | 81 | @doc """ 82 | Create a contact list via the SES V2 API, 83 | see (https://docs.aws.amazon.com/ses/latest/APIReference-V2/). 84 | 85 | ## Examples 86 | 87 | ExAws.SES.create_contact_list( 88 | "Test list", 89 | "Test description", 90 | tags: [%{"Key" => "environment", "Value" => "test"}], 91 | topics: [ 92 | %{ 93 | "TopicName": "test_topic" 94 | "DisplayName": "Test topic", 95 | "Description": "Test discription", 96 | "DefaultSubscriptionStatus": "OPT_IN", 97 | } 98 | ] 99 | ) 100 | 101 | """ 102 | @type create_contact_list_opt :: 103 | {:description, String.t()} 104 | | {:tags, [tag]} 105 | | {:topics, [%{(String.t() | atom) => String.t()}]} 106 | @spec create_contact_list(String.t(), opts :: [create_contact_list_opt]) :: 107 | ExAws.Operation.JSON.t() 108 | def create_contact_list(list_name, opts \\ []) do 109 | data = 110 | prune_map(%{ 111 | "ContactListName" => list_name, 112 | "Description" => opts[:description], 113 | "Tags" => opts[:tags], 114 | "Topics" => opts[:topics] 115 | }) 116 | 117 | request_v2(:post, "contact-lists") 118 | |> Map.put(:data, data) 119 | end 120 | 121 | @doc """ 122 | Update a contact list. Only accepts description and topic updates. 123 | 124 | ## Examples 125 | 126 | ExAws.SES.update_contact_list("test_list", description: "New description") 127 | 128 | """ 129 | @type topic :: %{ 130 | required(:DefaultSubscriptionStatus) => String.t(), 131 | optional(:Description) => String.t(), 132 | required(:DisplayName) => String.t(), 133 | required(:TopicName) => String.t() 134 | } 135 | @type update_contact_list_opt :: 136 | {:description, String.t()} 137 | | {:topics, [topic]} 138 | @spec update_contact_list(String.t(), opts :: [update_contact_list_opt]) :: ExAws.Operation.JSON.t() 139 | def update_contact_list(list_name, opts \\ []) do 140 | data = 141 | prune_map(%{ 142 | "ContactListName" => list_name, 143 | "Description" => opts[:description], 144 | "Topics" => opts[:topics] 145 | }) 146 | 147 | request_v2(:put, "contact-lists/#{list_name}") 148 | |> Map.put(:data, data) 149 | end 150 | 151 | @doc """ 152 | List contact lists. 153 | 154 | The API accepts pagination parameters, but they're redundant as AWS limits 155 | usage to a single list per account. 156 | """ 157 | @spec list_contact_lists() :: ExAws.Operation.JSON.t() 158 | def list_contact_lists() do 159 | request_v2(:get, "contact-lists") 160 | end 161 | 162 | @doc """ 163 | Show contact list. 164 | """ 165 | @spec get_contact_list(String.t()) :: ExAws.Operation.JSON.t() 166 | def get_contact_list(list_name) do 167 | request_v2(:get, "contact-lists/#{list_name}") 168 | end 169 | 170 | @doc """ 171 | Delete contact list. 172 | """ 173 | @spec delete_contact_list(String.t()) :: ExAws.Operation.JSON.t() 174 | def delete_contact_list(list_name) do 175 | request_v2(:delete, "contact-lists/#{list_name}") 176 | end 177 | 178 | ## Contacts 179 | ###################### 180 | 181 | @doc """ 182 | Create a new contact in a contact list. 183 | 184 | Options: 185 | 186 | * `:attributes` - arbitrary string to be assigned to AWS SES Contact AttributesData 187 | * `:topic_preferences` - list of maps for subscriptions to topics. 188 | SubscriptionStatus should be one of "OPT_IN" or "OPT_OUT" 189 | * `:unsubscribe_all` - causes contact to be unsubscribed from all topics 190 | """ 191 | @type topic_preference :: %{ 192 | TopicName: String.t(), 193 | SubscriptionStatus: String.t() 194 | } 195 | @type contact_opt :: 196 | {:attributes, String.t()} 197 | | {:topic_preferences, [topic_preference]} 198 | | {:unsubscribe_all, Boolean.t()} 199 | @spec create_contact(String.t(), email_address, [contact_opt]) :: ExAws.Operation.JSON.t() 200 | def create_contact(list_name, email, opts \\ []) do 201 | data = 202 | prune_map(%{ 203 | "EmailAddress" => email, 204 | "TopicPreferences" => opts[:topic_preferences], 205 | "AttributesData" => opts[:attributes], 206 | "UnsubscribeAll" => opts[:unsubscribe_all] 207 | }) 208 | 209 | request_v2(:post, "contact-lists/#{list_name}/contacts") 210 | |> Map.put(:data, data) 211 | end 212 | 213 | @doc """ 214 | Update a contact in a contact list. 215 | """ 216 | @spec update_contact(String.t(), email_address, [contact_opt]) :: ExAws.Operation.JSON.t() 217 | def update_contact(list_name, email, opts \\ []) do 218 | data = 219 | prune_map(%{ 220 | "TopicPreferences" => opts[:topic_preferences], 221 | "AttributesData" => opts[:attributes], 222 | "UnsubscribeAll" => opts[:unsubscribe_all] 223 | }) 224 | 225 | uri_encoded_email = ExAws.Request.Url.uri_encode(email) 226 | 227 | request_v2(:put, "contact-lists/#{list_name}/contacts/#{uri_encoded_email}") 228 | |> Map.put(:data, data) 229 | end 230 | 231 | @doc """ 232 | Show contacts in contact list. 233 | """ 234 | @spec list_contacts(String.t()) :: ExAws.Operation.JSON.t() 235 | def list_contacts(list_name) do 236 | request_v2(:get, "contact-lists/#{list_name}/contacts") 237 | end 238 | 239 | @doc """ 240 | Show a contact in a contact list. 241 | """ 242 | @spec get_contact(String.t(), email_address) :: ExAws.Operation.JSON.t() 243 | def get_contact(list_name, email) do 244 | uri_encoded_email = ExAws.Request.Url.uri_encode(email) 245 | request_v2(:get, "contact-lists/#{list_name}/contacts/#{uri_encoded_email}") 246 | end 247 | 248 | @doc """ 249 | Delete a contact in a contact list. 250 | """ 251 | @spec delete_contact(String.t(), email_address) :: ExAws.Operation.JSON.t() 252 | def delete_contact(list_name, email) do 253 | uri_encoded_email = ExAws.Request.Url.uri_encode(email) 254 | request_v2(:delete, "contact-lists/#{list_name}/contacts/#{uri_encoded_email}") 255 | end 256 | 257 | @doc """ 258 | Create a bulk import job to import contacts from S3. 259 | 260 | Params: 261 | 262 | * `:import_data_source` 263 | * `:import_destination` - requires either a `ContactListDestination` or 264 | `SuppressionListDestination` map. 265 | """ 266 | @type import_data_source :: %{DataFormat: String.t(), S3Url: String.t()} 267 | @type contact_list_destination :: %{ 268 | ContactListImportAction: String.t(), 269 | ContactListName: String.t() 270 | } 271 | @type suppression_list_destination :: %{SuppressionListImportAction: String.t()} 272 | @type import_destination :: %{ 273 | optional(:ContactListDestination) => contact_list_destination(), 274 | optional(:SuppressionListDestination) => suppression_list_destination() 275 | } 276 | @spec create_import_job(import_data_source(), import_destination()) :: ExAws.Operation.JSON.t() 277 | def create_import_job(data_source, destination) do 278 | data = %{ 279 | ImportDataSource: data_source, 280 | ImportDestination: destination 281 | } 282 | 283 | request_v2(:post, "import-jobs") 284 | |> Map.put(:data, data) 285 | end 286 | 287 | ## Suppression Lists 288 | ###################### 289 | @doc """ 290 | Add an email address to list of suppressed destinations. A suppression reason 291 | is mandatory (see `t:suppression_reason()`). 292 | """ 293 | @spec put_suppressed_destination(String.t(), SuppressionReason.t()) :: ExAws.Operation.JSON.t() 294 | def put_suppressed_destination(email_address, suppression_reason) do 295 | request_v2(:put, "suppression/addresses") 296 | |> Map.put(:data, %{ 297 | EmailAddress: email_address, 298 | Reason: suppression_reason 299 | }) 300 | end 301 | 302 | @doc """ 303 | Delete an email address from list of suppressed destinations. 304 | """ 305 | @spec delete_suppressed_destination(String.t()) :: ExAws.Operation.JSON.t() 306 | def delete_suppressed_destination(email_address) do 307 | uri_encoded_email_address = ExAws.Request.Url.uri_encode(email_address) 308 | 309 | request_v2(:delete, "suppression/addresses/#{uri_encoded_email_address}") 310 | end 311 | 312 | ## Templates 313 | ###################### 314 | 315 | @doc "Get email template" 316 | @spec get_template(String.t()) :: ExAws.Operation.Query.t() 317 | def get_template(template_name) do 318 | request(:get_template, %{"TemplateName" => template_name}) 319 | end 320 | 321 | @doc """ 322 | Get an email templates via V2 API. See https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_GetEmailTemplate.html 323 | """ 324 | @spec get_email_template(String.t()) :: ExAws.Operation.JSON.t() 325 | def get_email_template(template_name) do 326 | request_v2(:get, "templates/#{template_name}") 327 | end 328 | 329 | @type list_templates_opt :: 330 | {:max_items, pos_integer} 331 | | {:next_token, String.t()} 332 | 333 | @doc """ 334 | List email templates. 335 | """ 336 | @spec list_templates(opts :: [] | [list_templates_opt]) :: ExAws.Operation.Query.t() 337 | def list_templates(opts \\ []) do 338 | params = build_opts(opts, [:max_items, :next_token]) 339 | request(:list_templates, params) 340 | end 341 | 342 | @doc """ 343 | List email templates via V2 API. See https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_ListEmailTemplate.html 344 | """ 345 | @type list_templates_opt_v2 :: 346 | {:page_size, pos_integer} 347 | | {:next_token, String.t()} 348 | @spec list_email_templates(opts :: [] | [list_templates_opt_v2]) :: ExAws.Operation.JSON.t() 349 | def list_email_templates(opts \\ []) do 350 | params = build_opts(opts, [:page_size, :next_token]) 351 | request_v2(:get, "templates?#{URI.encode_query(params)}") 352 | end 353 | 354 | @doc """ 355 | Creates an email template. 356 | """ 357 | @type create_template_opt :: {:configuration_set_name, String.t()} 358 | @spec create_template(String.t(), String.t(), String.t(), String.t(), opts :: [create_template_opt]) :: 359 | ExAws.Operation.Query.t() 360 | def create_template(template_name, subject, html, text, opts \\ []) do 361 | template = 362 | %{ 363 | "TemplateName" => template_name, 364 | "SubjectPart" => subject 365 | } 366 | |> put_if_not_nil("HtmlPart", html) 367 | |> put_if_not_nil("TextPart", text) 368 | |> flatten_attrs("Template") 369 | 370 | params = 371 | opts 372 | |> build_opts([:configuration_set_name]) 373 | |> Map.merge(template) 374 | 375 | request(:create_template, params) 376 | end 377 | 378 | @doc """ 379 | Create an email template via V2 API. See https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_CreateEmailTemplate.html 380 | """ 381 | @spec create_email_template(String.t(), String.t(), String.t(), String.t()) :: ExAws.Operation.JSON.t() 382 | def create_email_template(template_name, subject, html, text) do 383 | template_content = 384 | %{ 385 | "Subject" => subject 386 | } 387 | |> put_if_not_nil("Html", html) 388 | |> put_if_not_nil("Text", text) 389 | 390 | params = 391 | %{ 392 | "TemplateName" => template_name, 393 | "TemplateContent" => template_content 394 | } 395 | 396 | request_v2(:post, "templates") 397 | |> Map.put(:data, params) 398 | end 399 | 400 | @doc """ 401 | Updates an email template. 402 | """ 403 | @type update_template_opt :: {:configuration_set_name, String.t()} 404 | @spec update_template(String.t(), String.t(), String.t(), String.t(), opts :: [update_template_opt]) :: 405 | ExAws.Operation.Query.t() 406 | def update_template(template_name, subject, html, text, opts \\ []) do 407 | template = 408 | %{ 409 | "TemplateName" => template_name, 410 | "SubjectPart" => subject 411 | } 412 | |> put_if_not_nil("HtmlPart", html) 413 | |> put_if_not_nil("TextPart", text) 414 | |> flatten_attrs("Template") 415 | 416 | params = 417 | opts 418 | |> build_opts([:configuration_set_name]) 419 | |> Map.merge(template) 420 | 421 | request(:update_template, params) 422 | end 423 | 424 | @doc """ 425 | Update an email template via V2 API. See https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_UpdateEmailTemplate.html 426 | """ 427 | @spec update_email_template(String.t(), String.t(), String.t(), String.t()) :: ExAws.Operation.JSON.t() 428 | def update_email_template(template_name, subject, html, text) do 429 | template_content = 430 | %{ 431 | "Subject" => subject 432 | } 433 | |> put_if_not_nil("Html", html) 434 | |> put_if_not_nil("Text", text) 435 | 436 | params = %{"TemplateContent" => template_content} 437 | 438 | request_v2(:put, "templates/#{template_name}") 439 | |> Map.put(:data, params) 440 | end 441 | 442 | @doc """ 443 | Deletes an email template. 444 | """ 445 | @spec delete_template(binary) :: ExAws.Operation.Query.t() 446 | def delete_template(template_name) do 447 | params = %{ 448 | "TemplateName" => template_name 449 | } 450 | 451 | request(:delete_template, params) 452 | end 453 | 454 | @doc """ 455 | Delete an email template via V2 API. See https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_DeleteEmailTemplate.html 456 | """ 457 | @spec delete_email_template(binary) :: ExAws.Operation.JSON.t() 458 | def delete_email_template(template_name) do 459 | request_v2(:delete, "templates/#{template_name}") 460 | end 461 | 462 | ## Emails 463 | ###################### 464 | 465 | @type email_address :: binary 466 | 467 | @type message :: %{ 468 | body: %{html: %{data: binary, charset: binary}, text: %{data: binary, charset: binary}}, 469 | subject: %{data: binary, charset: binary} 470 | } 471 | @type destination :: %{to: [email_address], cc: [email_address], bcc: [email_address]} 472 | 473 | @type bulk_destination :: [%{destination: destination, replacement_template_data: binary}] 474 | 475 | @type send_email_opt :: 476 | {:configuration_set_name, String.t()} 477 | | {:reply_to, [email_address]} 478 | | {:return_path, String.t()} 479 | | {:return_path_arn, String.t()} 480 | | {:source, String.t()} 481 | | {:source_arn, String.t()} 482 | | {:tags, %{(String.t() | atom) => String.t()}} 483 | 484 | @doc """ 485 | Composes an email message. 486 | """ 487 | @spec send_email(dst :: destination, msg :: message, src :: binary) :: ExAws.Operation.Query.t() 488 | @spec send_email(dst :: destination, msg :: message, src :: binary, opts :: [send_email_opt]) :: 489 | ExAws.Operation.Query.t() 490 | def send_email(dst, msg, src, opts \\ []) do 491 | params = 492 | opts 493 | |> build_opts([:configuration_set_name, :return_path, :return_path_arn, :source_arn, :bcc]) 494 | |> Map.merge(format_member_attribute(:reply_to_addresses, opts[:reply_to])) 495 | |> Map.merge(flatten_attrs(msg, "message")) 496 | |> Map.merge(format_tags(opts[:tags])) 497 | |> Map.merge(format_dst(dst)) 498 | |> Map.put_new("Source", src) 499 | 500 | request(:send_email, params) 501 | end 502 | 503 | @doc """ 504 | Send an email via the SES V2 API, which supports list management. 505 | 506 | `:content` should include one of a `Raw`, `Simple`, or `Template` key. 507 | """ 508 | @type destination_v2 :: %{ 509 | optional(:ToAddresses) => [email_address], 510 | optional(:CcAddresses) => [email_address], 511 | optional(:BccAddresses) => [email_address] 512 | } 513 | @type email_field :: %{optional(:Charset) => String.t(), required(:Data) => String.t()} 514 | @type email_content :: %{ 515 | optional(:Raw) => %{Data: binary}, 516 | optional(:Simple) => %{Body: %{Html: email_field, Text: email_field}, Subject: email_field}, 517 | optional(:Template) => %{TemplateArn: String.t(), TemplateData: String.t(), TemplateName: String.t()} 518 | } 519 | @type( 520 | send_email_v2_opt :: 521 | {:configuration_set_name, String.t()} 522 | | {:tags, [tag]} 523 | | {:feedback_forwarding_address, String.t()} 524 | | {:feedback_forwarding_arn, String.t()} 525 | | {:from_arn, String.t()} 526 | | {:list_management, %{ContactListName: String.t(), TopicName: String.t()}} 527 | | {:reply_addresses, [String.t()]}, 528 | @spec(send_email_v2(destination_v2, email_content, email_address, [send_email_v2_opt])) 529 | ) 530 | def send_email_v2(destination, content, from_email, opts \\ []) do 531 | data = 532 | prune_map(%{ 533 | ConfigurationSetName: opts[:configuration_set_name], 534 | Content: content, 535 | Destination: destination, 536 | EmailTags: opts[:tags], 537 | FeedbackForwardingEmailAddress: opts[:feedback_forwarding_address], 538 | FeedbackForwardingEmailAddressIdentityArn: opts[:feedback_forwarding_arn], 539 | FromEmailAddress: from_email, 540 | FromEmailAddressIdentityArn: opts[:from_arn], 541 | ListManagementOptions: opts[:list_management], 542 | ReplyToAddresses: opts[:reply_addresses] 543 | }) 544 | 545 | request_v2(:post, "outbound-emails") 546 | |> Map.put(:data, data) 547 | end 548 | 549 | @doc """ 550 | Send a raw Email. 551 | """ 552 | @type send_raw_email_opt :: 553 | {:configuration_set_name, String.t()} 554 | | {:from_arn, String.t()} 555 | | {:return_path_arn, String.t()} 556 | | {:source, String.t()} 557 | | {:source_arn, String.t()} 558 | | {:tags, %{(String.t() | atom) => String.t()}} 559 | 560 | @spec send_raw_email(binary, opts :: [send_raw_email_opt]) :: ExAws.Operation.Query.t() 561 | def send_raw_email(raw_msg, opts \\ []) do 562 | params = 563 | opts 564 | |> build_opts([:configuration_set_name, :from_arn, :return_path_arn, :source, :source_arn]) 565 | |> Map.merge(format_tags(opts[:tags])) 566 | |> Map.put("RawMessage.Data", Base.encode64(raw_msg)) 567 | 568 | request(:send_raw_email, params) 569 | end 570 | 571 | @doc """ 572 | Send a templated Email. 573 | """ 574 | @type send_templated_email_opt :: 575 | {:configuration_set_name, String.t()} 576 | | {:return_path, String.t()} 577 | | {:return_path_arn, String.t()} 578 | | {:source, String.t()} 579 | | {:source_arn, String.t()} 580 | | {:reply_to, [email_address]} 581 | | {:tags, %{(String.t() | atom) => String.t()}} 582 | 583 | @spec send_templated_email( 584 | dst :: destination, 585 | src :: binary, 586 | template :: binary, 587 | template_data :: map, 588 | opts :: [send_templated_email_opt] 589 | ) :: ExAws.Operation.Query.t() 590 | def send_templated_email(dst, src, template, template_data, opts \\ []) do 591 | params = 592 | opts 593 | |> build_opts([:configuration_set_name, :return_path, :return_path_arn, :source_arn, :bcc]) 594 | |> Map.merge(format_member_attribute(:reply_to_addresses, opts[:reply_to])) 595 | |> Map.merge(format_tags(opts[:tags])) 596 | |> Map.merge(format_dst(dst)) 597 | |> Map.put("Source", src) 598 | |> Map.put("Template", template) 599 | |> Map.put("TemplateData", format_template_data(template_data)) 600 | 601 | request(:send_templated_email, params) 602 | end 603 | 604 | @doc """ 605 | Send a templated email to multiple destinations. 606 | """ 607 | @type send_bulk_templated_email_opt :: 608 | {:configuration_set_name, String.t()} 609 | | {:return_path, String.t()} 610 | | {:return_path_arn, String.t()} 611 | | {:source_arn, String.t()} 612 | | {:default_template_data, String.t()} 613 | | {:reply_to, [email_address]} 614 | | {:tags, %{(String.t() | atom) => String.t()}} 615 | 616 | @spec send_bulk_templated_email( 617 | template :: binary, 618 | source :: binary, 619 | destinations :: bulk_destination, 620 | opts :: [send_bulk_templated_email_opt] 621 | ) :: ExAws.Operation.Query.t() 622 | def send_bulk_templated_email(template, source, destinations, opts \\ []) do 623 | params = 624 | opts 625 | |> build_opts([:configuration_set_name, :return_path, :return_path_arn, :source_arn, :default_template_data]) 626 | |> Map.merge(format_member_attribute(:reply_to_addresses, opts[:reply_to])) 627 | |> Map.merge(format_tags(opts[:tags])) 628 | |> Map.merge(format_bulk_destinations(destinations)) 629 | |> Map.put("DefaultTemplateData", format_template_data(opts[:default_template_data])) 630 | |> Map.put("Source", source) 631 | |> Map.put("Template", template) 632 | 633 | request(:send_bulk_templated_email, params) 634 | end 635 | 636 | @doc """ 637 | Deletes the specified identity (an email address or a domain) from the list 638 | of verified identities. 639 | """ 640 | @spec delete_identity(binary) :: ExAws.Operation.Query.t() 641 | def delete_identity(identity) do 642 | request(:delete_identity, %{"Identity" => identity}) 643 | end 644 | 645 | @type set_identity_notification_topic_opt :: {:sns_topic, binary} 646 | @type notification_type :: :bounce | :complaint | :delivery 647 | 648 | @doc """ 649 | Sets the Amazon Simple Notification Service (Amazon SNS) topic to which 650 | Amazon SES will publish delivery notifications for emails sent with given 651 | identity. 652 | 653 | Absent `:sns_topic` options cleans SnsTopic and disables publishing. 654 | 655 | Notification type can be on of the `:bounce`, `:complaint`, or `:delivery`. 656 | Requests are throttled to one per second. 657 | """ 658 | @spec set_identity_notification_topic(binary, notification_type, set_identity_notification_topic_opt | []) :: 659 | ExAws.Operation.Query.t() 660 | def set_identity_notification_topic(identity, type, opts \\ []) when type in @notification_types do 661 | notification_type = Atom.to_string(type) |> String.capitalize() 662 | 663 | params = 664 | opts 665 | |> build_opts([:sns_topic]) 666 | |> Map.merge(%{"Identity" => identity, "NotificationType" => notification_type}) 667 | 668 | request(:set_identity_notification_topic, params) 669 | end 670 | 671 | @doc """ 672 | Enables or disables whether Amazon SES forwards notifications as email. 673 | """ 674 | @spec set_identity_feedback_forwarding_enabled(boolean, binary) :: ExAws.Operation.Query.t() 675 | def set_identity_feedback_forwarding_enabled(enabled, identity) do 676 | request(:set_identity_feedback_forwarding_enabled, %{"ForwardingEnabled" => enabled, "Identity" => identity}) 677 | end 678 | 679 | @doc """ 680 | Build message object. 681 | """ 682 | @spec build_message(binary, binary, binary, binary) :: message 683 | def build_message(html, txt, subject, charset \\ "UTF-8") do 684 | %{ 685 | body: %{ 686 | html: %{data: html, charset: charset}, 687 | text: %{data: txt, charset: charset} 688 | }, 689 | subject: %{data: subject, charset: charset} 690 | } 691 | end 692 | 693 | @doc """ 694 | Set whether SNS notifications should include original email headers or not. 695 | """ 696 | @spec set_identity_headers_in_notifications_enabled(binary, notification_type, boolean) :: ExAws.Operation.Query.t() 697 | def set_identity_headers_in_notifications_enabled(identity, type, enabled) do 698 | notification_type = Atom.to_string(type) |> String.capitalize() 699 | 700 | request( 701 | :set_identity_headers_in_notifications_enabled, 702 | %{"Identity" => identity, "NotificationType" => notification_type, "Enabled" => enabled} 703 | ) 704 | end 705 | 706 | @doc "Create a custom verification email template." 707 | @spec create_custom_verification_email_template( 708 | String.t(), 709 | String.t(), 710 | String.t(), 711 | String.t(), 712 | String.t(), 713 | String.t() 714 | ) :: ExAws.Operation.Query.t() 715 | def create_custom_verification_email_template( 716 | template_name, 717 | from_email_address, 718 | template_subject, 719 | template_content, 720 | success_redirection_url, 721 | failure_redirection_url 722 | ) do 723 | request(:create_custom_verification_email_template, %{ 724 | "TemplateName" => template_name, 725 | "FromEmailAddress" => from_email_address, 726 | "TemplateSubject" => template_subject, 727 | "TemplateContent" => template_content, 728 | "SuccessRedirectionURL" => success_redirection_url, 729 | "FailureRedirectionURL" => failure_redirection_url 730 | }) 731 | end 732 | 733 | @type update_custom_verification_email_template_opt :: 734 | {:template_name, String.t()} 735 | | {:from_email_address, String.t()} 736 | | {:template_subject, String.t()} 737 | | {:template_content, String.t()} 738 | | {:success_redirection_url, String.t()} 739 | | {:failure_redirection_url, String.t()} 740 | @doc "Update or create a custom verification email template." 741 | @spec update_custom_verification_email_template(opts :: [update_custom_verification_email_template_opt] | []) :: 742 | ExAws.Operation.Query.t() 743 | def update_custom_verification_email_template(opts \\ []) do 744 | params = 745 | opts 746 | |> build_opts([ 747 | :template_name, 748 | :from_email_address, 749 | :template_subject, 750 | :template_content 751 | ]) 752 | |> maybe_put_param(opts, :success_redirection_url, "SuccessRedirectionURL") 753 | |> maybe_put_param(opts, :failure_redirection_url, "FailureRedirectionURL") 754 | 755 | request(:update_custom_verification_email_template, params) 756 | end 757 | 758 | @doc "Delete custom verification email template." 759 | @spec delete_custom_verification_email_template(String.t()) :: ExAws.Operation.Query.t() 760 | def delete_custom_verification_email_template(template_name) do 761 | request(:delete_custom_verification_email_template, %{"TemplateName" => template_name}) 762 | end 763 | 764 | @type list_custom_verification_email_templates_opt :: {:max_results, String.t()} | {:next_token, String.t()} 765 | @doc "Lists custom verification email templates." 766 | @spec list_custom_verification_email_templates(opts :: [list_custom_verification_email_templates_opt()] | []) :: 767 | ExAws.Operation.Query.t() 768 | def list_custom_verification_email_templates(opts \\ []) do 769 | params = build_opts(opts, [:max_results, :next_token]) 770 | request(:list_custom_verification_email_templates, params) 771 | end 772 | 773 | @type send_custom_verification_email_opt :: {:configuration_set_name, String.t()} 774 | @doc "Send a verification email using a custom template." 775 | @spec send_custom_verification_email(String.t(), String.t(), opts :: [send_custom_verification_email_opt] | []) :: 776 | ExAws.Operation.Query.t() 777 | def send_custom_verification_email(email_address, template_name, opts \\ []) do 778 | params = 779 | opts 780 | |> build_opts([:configuration_set_name]) 781 | |> Map.put("EmailAddress", email_address) 782 | |> Map.put("TemplateName", template_name) 783 | 784 | request(:send_custom_verification_email, params) 785 | end 786 | 787 | @doc "Create a custom verification email template via the SES V2 API. See https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_CreateCustomVerificationEmailTemplate.html" 788 | @spec create_custom_verification_email_template_v2( 789 | String.t(), 790 | String.t(), 791 | String.t(), 792 | String.t(), 793 | String.t(), 794 | String.t() 795 | ) :: ExAws.Operation.JSON.t() 796 | def create_custom_verification_email_template_v2( 797 | template_name, 798 | from_email_address, 799 | template_subject, 800 | template_content, 801 | success_redirection_url, 802 | failure_redirection_url 803 | ) do 804 | request_v2(:post, "/custom-verification-email-templates") 805 | |> Map.put(:data, %{ 806 | "TemplateName" => template_name, 807 | "FromEmailAddress" => from_email_address, 808 | "TemplateSubject" => template_subject, 809 | "TemplateContent" => template_content, 810 | "SuccessRedirectionURL" => success_redirection_url, 811 | "FailureRedirectionURL" => failure_redirection_url 812 | }) 813 | end 814 | 815 | @doc "Update an existing custom verification email template via the SES V2 API. See https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_UpdateCustomVerificationEmailTemplate.html" 816 | @spec update_custom_verification_email_template_v2( 817 | String.t(), 818 | opts :: [update_custom_verification_email_template_opt] | [] 819 | ) :: 820 | ExAws.Operation.JSON.t() 821 | def update_custom_verification_email_template_v2(template_name, opts \\ []) do 822 | params = 823 | opts 824 | |> build_opts([ 825 | :from_email_address, 826 | :template_subject, 827 | :template_content 828 | ]) 829 | |> maybe_put_param(opts, :success_redirection_url, "SuccessRedirectionURL") 830 | |> maybe_put_param(opts, :failure_redirection_url, "FailureRedirectionURL") 831 | 832 | request_v2(:put, "custom-verification-email-templates/#{template_name}") 833 | |> Map.put(:data, params) 834 | end 835 | 836 | @doc "Deletes an existing custom verification email template via the SES V2 API. See https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_DeleteCustomVerificationEmailTemplate.html" 837 | @spec delete_custom_verification_email_template_v2(String.t()) :: ExAws.Operation.JSON.t() 838 | def delete_custom_verification_email_template_v2(template_name) do 839 | request_v2(:delete, "custom-verification-email-templates/#{template_name}") 840 | end 841 | 842 | @type list_custom_verification_email_templates_opt_v2 :: {:page_size, pos_integer} | {:next_token, String.t()} 843 | @doc "Lists the existing custom verification email templates via the SES V2 API. See https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_ListCustomVerificationEmailTemplates.html" 844 | @spec list_custom_verification_email_templates_v2(opts :: [list_custom_verification_email_templates_opt_v2()] | []) :: 845 | ExAws.Operation.JSON.t() 846 | def list_custom_verification_email_templates_v2(opts \\ []) do 847 | params = build_opts(opts, [:page_size, :next_token]) 848 | request_v2(:get, "custom-verification-email-templates?#{URI.encode_query(params)}") 849 | end 850 | 851 | @doc "Get a custom verification email template via the SES V2 API. See https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_GetCustomVerificationEmailTemplate.html" 852 | @spec get_custom_verification_email_template_v2(String.t()) :: ExAws.Operation.JSON.t() 853 | def get_custom_verification_email_template_v2(template_name) do 854 | request_v2(:get, "custom-verification-email-templates/#{template_name}") 855 | end 856 | 857 | @doc "Send a verification email using a custom template via SES V2 API. See https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_SendCustomVerificationEmail.html" 858 | @spec send_custom_verification_email_v2(String.t(), String.t(), opts :: [send_custom_verification_email_opt] | []) :: 859 | ExAws.Operation.JSON.t() 860 | def send_custom_verification_email_v2(email_address, template_name, opts \\ []) do 861 | params = 862 | opts 863 | |> build_opts([:configuration_set_name]) 864 | |> Map.put("EmailAddress", email_address) 865 | |> Map.put("TemplateName", template_name) 866 | 867 | request_v2(:post, "outbound-custom-verification-emails") 868 | |> Map.put(:data, params) 869 | end 870 | 871 | @spec test_render_email_template(String.t(), map()) :: ExAws.Operation.JSON.t() 872 | def test_render_email_template(template_name, template_data) do 873 | request_v2(:post, "templates/#{template_name}/render") 874 | |> Map.put(:data, %{ 875 | "TemplateData" => format_template_data(template_data) 876 | }) 877 | end 878 | 879 | ## Receipt Rules and Rule Sets 880 | ###################### 881 | @doc "Describe the given receipt rule set." 882 | @spec describe_receipt_rule_set(String.t()) :: ExAws.Operation.Query.t() 883 | def describe_receipt_rule_set(rule_set_name) do 884 | request(:describe_receipt_rule_set, %{"RuleSetName" => rule_set_name}) 885 | end 886 | 887 | defp format_dst(dst, root \\ "destination") do 888 | dst = 889 | Enum.reduce([:to, :bcc, :cc], %{}, fn key, acc -> 890 | case Map.fetch(dst, key) do 891 | {:ok, val} -> Map.put(acc, :"#{key}_addresses", val) 892 | _ -> acc 893 | end 894 | end) 895 | 896 | dst 897 | |> Map.to_list() 898 | |> format_member_attributes([:bcc_addresses, :cc_addresses, :to_addresses]) 899 | |> flatten_attrs(root) 900 | end 901 | 902 | defp format_template_data(nil), do: "{}" 903 | 904 | defp format_template_data(template_data), do: Map.get(aws_base_config(), :json_codec).encode!(template_data) 905 | 906 | defp format_bulk_destinations(destinations) do 907 | destinations 908 | |> Enum.with_index(1) 909 | |> Enum.flat_map(fn 910 | {%{destination: destination} = destination_member, index} -> 911 | root = "Destinations.member.#{index}" 912 | 913 | destination 914 | |> format_dst("#{root}.Destination") 915 | |> add_replacement_template_data(destination_member, root) 916 | |> Map.to_list() 917 | end) 918 | |> Map.new() 919 | end 920 | 921 | defp add_replacement_template_data(destination, %{replacement_template_data: replacement_template_data}, root) do 922 | destination 923 | |> Map.put("#{root}.ReplacementTemplateData", format_template_data(replacement_template_data)) 924 | end 925 | 926 | defp add_replacement_template_data(destination, _, _), do: destination 927 | 928 | defp format_tags(nil), do: %{} 929 | 930 | defp format_tags(tags) do 931 | tags 932 | |> Enum.with_index(1) 933 | |> Enum.reduce(%{}, fn {tag, index}, acc -> 934 | key = camelize_key("tags.member.#{index}") 935 | Map.merge(acc, flatten_attrs(tag, key)) 936 | end) 937 | end 938 | 939 | ## Request 940 | ###################### 941 | 942 | defp request(action, params) do 943 | action_string = action |> Atom.to_string() |> Macro.camelize() 944 | 945 | %ExAws.Operation.Query{ 946 | path: "/", 947 | params: params |> Map.put("Action", action_string), 948 | service: @service, 949 | action: action, 950 | parser: &ExAws.SES.Parsers.parse/2 951 | } 952 | end 953 | 954 | defp request_v2(method) do 955 | %ExAws.Operation.JSON{ 956 | http_method: method, 957 | path: @v2_path, 958 | service: @service 959 | } 960 | end 961 | 962 | defp request_v2(method, resource) do 963 | request_v2(method) 964 | |> Map.put(:path, @v2_path <> "/#{resource}") 965 | end 966 | 967 | defp build_opts(opts, permitted) do 968 | opts 969 | |> Map.new() 970 | |> Map.take(permitted) 971 | |> camelize_keys 972 | end 973 | 974 | defp maybe_put_param(params, opts, key, name) do 975 | case opts[key] do 976 | nil -> params 977 | value -> Map.put(params, name, value) 978 | end 979 | end 980 | 981 | defp format_member_attributes(opts, members) do 982 | opts 983 | |> Map.new() 984 | |> Map.take(members) 985 | |> Enum.reduce(Map.new(opts), fn entry, acc -> Map.merge(acc, format_member_attribute(entry)) end) 986 | |> Map.drop(members) 987 | end 988 | 989 | defp format_member_attribute(key, collection), do: format_member_attribute({key, collection}) 990 | 991 | defp format_member_attribute({_, nil}), do: %{} 992 | 993 | defp format_member_attribute({key, collection}) do 994 | collection 995 | |> Enum.with_index(1) 996 | |> Map.new(fn {item, index} -> 997 | {"#{camelize_key(key)}.member.#{index}", item} 998 | end) 999 | end 1000 | 1001 | defp flatten_attrs(attrs, root) do 1002 | do_flatten_attrs({attrs, camelize_key(root)}) 1003 | |> List.flatten() 1004 | |> Map.new() 1005 | end 1006 | 1007 | defp do_flatten_attrs({attrs, root}) when is_map(attrs) do 1008 | Enum.map(attrs, fn {k, v} -> 1009 | do_flatten_attrs({v, root <> "." <> camelize_key(k)}) 1010 | end) 1011 | end 1012 | 1013 | defp do_flatten_attrs({val, path}) do 1014 | {camelize_key(path), val} 1015 | end 1016 | 1017 | defp put_if_not_nil(map, _, nil), do: map 1018 | defp put_if_not_nil(map, key, value), do: map |> Map.put(key, value) 1019 | 1020 | defp aws_base_config(), do: ExAws.Config.build_base(@service) 1021 | 1022 | defp prune_map(map) do 1023 | map 1024 | |> Enum.reject(fn {_k, v} -> is_nil(v) end) 1025 | |> Map.new() 1026 | end 1027 | end 1028 | -------------------------------------------------------------------------------- /lib/ex_aws/ses/parsers.ex: -------------------------------------------------------------------------------- 1 | if Code.ensure_loaded?(SweetXml) do 2 | defmodule ExAws.SES.Parsers do 3 | import SweetXml, only: [sigil_x: 2] 4 | 5 | @moduledoc false 6 | 7 | def parse({:ok, %{body: xml} = resp}, :verify_email_identity) do 8 | parsed_body = SweetXml.xpath(xml, ~x"//VerifyEmailIdentityResponse", request_id: request_id_xpath()) 9 | 10 | {:ok, Map.put(resp, :body, parsed_body)} 11 | end 12 | 13 | def parse({:ok, %{body: xml} = resp}, :verify_domain_identity) do 14 | parsed_body = SweetXml.xpath(xml, ~x"//VerifyDomainIdentityResponse", 15 | verification_token: ~x"./VerifyDomainIdentityResult/VerificationToken/text()"s, 16 | request_id: request_id_xpath() 17 | ) 18 | 19 | {:ok, Map.put(resp, :body, parsed_body)} 20 | end 21 | 22 | def parse({:ok, %{body: xml} = resp}, :verify_domain_dkim) do 23 | parsed_body = SweetXml.xpath(xml, ~x"//VerifyDomainDkimResponse", 24 | dkim_tokens: [ 25 | ~x"./VerifyDomainDkimResult", 26 | members: ~x"./DkimTokens/member/text()"ls 27 | ], 28 | request_id: request_id_xpath() 29 | ) 30 | 31 | {:ok, Map.put(resp, :body, parsed_body)} 32 | end 33 | 34 | 35 | def parse({:ok, %{body: xml} = resp}, :get_identity_verification_attributes) do 36 | parsed_body = 37 | xml 38 | |> SweetXml.xpath(~x"//GetIdentityVerificationAttributesResponse", 39 | verification_attributes: [ 40 | ~x"./GetIdentityVerificationAttributesResult/VerificationAttributes/entry"l, 41 | entry: ~x"./key/text()"s, 42 | verification_status: ~x"./value/VerificationStatus/text()"s, 43 | verification_token: ~x"./value/VerificationToken/text()"so 44 | ], 45 | request_id: request_id_xpath() 46 | ) 47 | |> update_in([:verification_attributes], &verification_attributes_list_to_map/1) 48 | 49 | {:ok, Map.put(resp, :body, parsed_body)} 50 | end 51 | 52 | def parse({:ok, %{body: xml} = resp}, :list_identities) do 53 | parsed_body = 54 | xml 55 | |> SweetXml.xpath(~x"//ListIdentitiesResponse", 56 | custom_verification_email_templates: [ 57 | ~x"./ListIdentitiesResult", 58 | members: ~x"./Identities/member/text()"ls, 59 | next_token: ~x"./NextToken/text()"so 60 | ], #TODO: Remove this key in the next major version, 3.x.x 61 | identities: [ 62 | ~x"./ListIdentitiesResult", 63 | members: ~x"./Identities/member/text()"ls, 64 | next_token: ~x"./NextToken/text()"so 65 | ], 66 | request_id: request_id_xpath() 67 | ) 68 | 69 | {:ok, Map.put(resp, :body, parsed_body)} 70 | end 71 | 72 | def parse({:ok, %{body: xml} = resp}, :list_configuration_sets) do 73 | parsed_body = 74 | xml 75 | |> SweetXml.xpath(~x"//ListConfigurationSetsResponse", 76 | configuration_sets: [ 77 | ~x"./ListConfigurationSetsResult", 78 | members: ~x"./ConfigurationSets/member/Name/text()"ls, 79 | next_token: ~x"./NextToken/text()"so 80 | ], 81 | request_id: request_id_xpath() 82 | ) 83 | 84 | {:ok, Map.put(resp, :body, parsed_body)} 85 | end 86 | 87 | def parse({:ok, %{body: xml} = resp}, :send_email) do 88 | parsed_body = 89 | xml 90 | |> SweetXml.xpath(~x"//SendEmailResponse", 91 | message_id: ~x"./SendEmailResult/MessageId/text()"s, 92 | request_id: request_id_xpath() 93 | ) 94 | 95 | {:ok, Map.put(resp, :body, parsed_body)} 96 | end 97 | 98 | def parse({:ok, %{body: xml} = resp}, :send_templated_email) do 99 | parsed_body = 100 | xml 101 | |> SweetXml.xpath(~x"//SendTemplatedEmailResponse", 102 | message_id: ~x"./SendTemplatedEmailResult/MessageId/text()"s, 103 | request_id: request_id_xpath() 104 | ) 105 | 106 | {:ok, Map.put(resp, :body, parsed_body)} 107 | end 108 | 109 | def parse({:ok, %{body: xml} = resp}, :send_bulk_templated_email) do 110 | parsed_body = 111 | xml 112 | |> SweetXml.xmap( 113 | messages: [ 114 | ~x[//SendBulkTemplatedEmailResponse/SendBulkTemplatedEmailResult/Status/member]l, 115 | message_id: ~x"./MessageId/text()"s, 116 | status: ~x"./Status/text()"s 117 | ], 118 | request_id: request_id_xpath() 119 | ) 120 | 121 | {:ok, Map.put(resp, :body, parsed_body)} 122 | end 123 | 124 | def parse({:ok, %{body: xml} = resp}, :send_raw_email) do 125 | parsed_body = 126 | xml 127 | |> SweetXml.xpath(~x"//SendRawEmailResponse", 128 | message_id: ~x"./SendRawEmailResult/MessageId/text()"s, 129 | request_id: request_id_xpath() 130 | ) 131 | 132 | {:ok, Map.put(resp, :body, parsed_body)} 133 | end 134 | 135 | def parse({:ok, %{body: xml} = resp}, :delete_identity) do 136 | parsed_body = SweetXml.xpath(xml, ~x"//DeleteIdentityResponse", request_id: request_id_xpath()) 137 | 138 | {:ok, Map.put(resp, :body, parsed_body)} 139 | end 140 | 141 | def parse({:ok, %{body: xml} = resp}, :set_identity_notification_topic) do 142 | parsed_body = SweetXml.xpath(xml, ~x"//SetIdentityNotificationTopicResponse", request_id: request_id_xpath()) 143 | 144 | {:ok, Map.put(resp, :body, parsed_body)} 145 | end 146 | 147 | def parse({:ok, %{body: xml} = resp}, :set_identity_feedback_forwarding_enabled) do 148 | parsed_body = 149 | SweetXml.xpath( 150 | xml, 151 | ~x"//SetIdentityFeedbackForwardingEnabledResponse", 152 | request_id: request_id_xpath() 153 | ) 154 | 155 | {:ok, Map.put(resp, :body, parsed_body)} 156 | end 157 | 158 | def parse({:ok, %{body: xml} = resp}, :set_identity_headers_in_notifications_enabled) do 159 | parsed_body = 160 | SweetXml.xpath( 161 | xml, 162 | ~x"//SetIdentityHeadersInNotificationsEnabledResponse", 163 | request_id: request_id_xpath() 164 | ) 165 | 166 | {:ok, Map.put(resp, :body, parsed_body)} 167 | end 168 | 169 | def parse({:ok, %{body: xml} = resp}, :create_template) do 170 | parsed_body = SweetXml.xpath(xml, ~x"//CreateTemplateResponse", request_id: request_id_xpath()) 171 | 172 | {:ok, Map.put(resp, :body, parsed_body)} 173 | end 174 | 175 | def parse({:ok, %{body: xml} = resp}, :delete_template) do 176 | parsed_body = SweetXml.xpath(xml, ~x"//DeleteTemplateResponse", request_id: request_id_xpath()) 177 | 178 | {:ok, Map.put(resp, :body, parsed_body)} 179 | end 180 | 181 | def parse({:ok, %{body: xml} = resp}, :list_custom_verification_email_templates) do 182 | parsed_body = 183 | xml 184 | |> SweetXml.xpath(~x"//ListCustomVerificationEmailTemplatesResponse", 185 | custom_verification_email_templates: [ 186 | ~x"./ListCustomVerificationEmailTemplatesResult", 187 | members: [ 188 | ~x"./CustomVerificationEmailTemplates/member"l, 189 | template_name: ~x"./TemplateName/text()"so, 190 | from_email_address: ~x"./FromEmailAddress/text()"so, 191 | template_subject: ~x"./TemplateSubject/text()"so, 192 | success_redirection_url: ~x"./SuccessRedirectionURL/text()"so, 193 | failure_redirection_url: ~x"./FailureRedirectionURL/text()"so 194 | ], 195 | next_token: ~x"./NextToken/text()"so 196 | ], 197 | request_id: request_id_xpath() 198 | ) 199 | 200 | {:ok, Map.put(resp, :body, parsed_body)} 201 | end 202 | 203 | def parse({:ok, %{body: xml} = resp}, :describe_receipt_rule_set) do 204 | parsed_body = SweetXml.xpath(xml, ~x"//DescribeReceiptRuleSetResponse", 205 | rules: [ 206 | ~x"./DescribeReceiptRuleSetResult", 207 | members: [ 208 | ~x"./Rules/member"l, 209 | enabled: ~x"./Enabled/text()"so |> transform_to_boolean(), 210 | name: ~x"./Name/text()"s, 211 | recipients: ~x"./Recipients/member/text()"ls, 212 | scan_enabled: ~x"./ScanEnabled/text()"so |> transform_to_boolean(), 213 | tls_policy: ~x"./TlsPolicy/text()"so, 214 | ], 215 | ], 216 | request_id: request_id_xpath() 217 | ) 218 | 219 | {:ok, Map.put(resp, :body, parsed_body)} 220 | end 221 | 222 | def parse({:error, {type, http_status_code, %{body: xml}}}, _) do 223 | parsed_body = 224 | xml 225 | |> SweetXml.xpath(~x"//ErrorResponse", 226 | request_id: ~x"./RequestId/text()"s, 227 | type: ~x"./Error/Type/text()"s, 228 | code: ~x"./Error/Code/text()"s, 229 | message: ~x"./Error/Message/text()"s, 230 | detail: ~x"./Error/Detail/text()"s 231 | ) 232 | 233 | {:error, {type, http_status_code, parsed_body}} 234 | end 235 | 236 | def parse(val, _), do: val 237 | 238 | defp request_id_xpath do 239 | ~x"./ResponseMetadata/RequestId/text()"s 240 | end 241 | 242 | defp verification_attributes_list_to_map(attributes) do 243 | Enum.reduce(attributes, %{}, fn %{entry: key} = attribute, acc -> 244 | props = 245 | attribute 246 | |> Map.delete(:entry) 247 | |> Enum.reject(fn kv -> elem(kv, 1) in ["", nil] end) 248 | |> Enum.into(%{}) 249 | 250 | Map.put_new(acc, key, props) 251 | end) 252 | end 253 | 254 | defp transform_to_boolean(arg) do 255 | SweetXml.transform_by(arg, fn 256 | "false" -> false 257 | "true" -> true 258 | _ -> nil 259 | end) 260 | end 261 | end 262 | else 263 | defmodule ExAws.SES.Parsers do 264 | @moduledoc false 265 | 266 | def parse(val, _), do: val 267 | end 268 | end 269 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ExAws.SES.Mixfile do 2 | use Mix.Project 3 | 4 | @version "2.4.1" 5 | @service "ses" 6 | @url "https://github.com/ex-aws/ex_aws_#{@service}" 7 | @name __MODULE__ |> Module.split() |> Enum.take(2) |> Enum.join(".") 8 | 9 | def project do 10 | [ 11 | app: :ex_aws_ses, 12 | version: @version, 13 | elixir: "~> 1.5", 14 | elixirc_paths: elixirc_paths(Mix.env()), 15 | start_permanent: Mix.env() == :prod, 16 | deps: deps(), 17 | name: @name, 18 | package: package(), 19 | docs: docs() 20 | ] 21 | end 22 | 23 | defp elixirc_paths(:test), do: ["lib", "test/support"] 24 | defp elixirc_paths(_), do: ["lib"] 25 | 26 | # Run "mix help compile.app" to learn about applications. 27 | def application do 28 | [ 29 | extra_applications: [:logger] 30 | ] 31 | end 32 | 33 | defp package do 34 | [ 35 | description: "#{@name} service package", 36 | files: ["lib", "config", "mix.exs", "CHANGELOG*", "README*"], 37 | maintainers: ["Ben Wilson"], 38 | licenses: ["MIT"], 39 | links: %{ 40 | Changelog: "https://hexdocs.pm/ex_aws_ses/changelog.html", 41 | GitHub: @url 42 | } 43 | ] 44 | end 45 | 46 | # Run "mix help deps" to learn about dependencies. 47 | defp deps do 48 | [ 49 | {:hackney, ">= 0.0.0", only: [:dev, :test]}, 50 | {:sweet_xml, ">= 0.0.0", only: [:dev, :test]}, 51 | {:jason, ">= 0.0.0", only: [:dev, :test]}, 52 | {:dialyxir, "~> 1.0.0-rc.3", only: [:dev], runtime: false}, 53 | {:ex_doc, ">= 0.0.0", only: [:dev, :test], runtime: false}, 54 | ex_aws() 55 | ] 56 | end 57 | 58 | defp docs do 59 | [ 60 | extras: ["CHANGELOG.md", "README.md"], 61 | main: "readme", 62 | source_url: @url, 63 | source_ref: "v#{@version}", 64 | formatters: ["html"] 65 | ] 66 | end 67 | 68 | defp ex_aws() do 69 | case System.get_env("AWS") do 70 | "LOCAL" -> {:ex_aws, path: "../ex_aws"} 71 | _ -> {:ex_aws, "~> 2.0"} 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, 3 | "dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"}, 4 | "earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm", "000aaeff08919e95e7aea13e4af7b2b9734577b3e6a7c50ee31ee88cab6ec4fb"}, 5 | "earmark_parser": {:hex, :earmark_parser, "1.4.24", "344f8d2a558691d3fcdef3f9400157d7c4b3b8e58ee5063297e9ae593e8326d9", [:mix], [], "hexpm", "1f6451b0116dd270449c8f5b30289940ee9c0a39154c783283a08e55af82ea34"}, 6 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 7 | "ex_aws": {:hex, :ex_aws, "2.2.10", "064139724335b00b6665af7277189afc9ed507791b1ccf2698dadc7c8ad892e8", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "98acb63f74b2f0822be219c5c2f0e8d243c2390f5325ad0557b014d3360da47e"}, 8 | "ex_doc": {:hex, :ex_doc, "0.28.2", "e031c7d1a9fc40959da7bf89e2dc269ddc5de631f9bd0e326cbddf7d8085a9da", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "51ee866993ffbd0e41c084a7677c570d0fc50cb85c6b5e76f8d936d9587fa719"}, 9 | "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, 10 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 11 | "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, 12 | "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, 13 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, 14 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 15 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 16 | "mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"}, 17 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 18 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, 19 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 20 | "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"}, 21 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 22 | "sweet_xml": {:hex, :sweet_xml, "0.7.2", "4729f997286811fabdd8288f8474e0840a76573051062f066c4b597e76f14f9f", [:mix], [], "hexpm", "6894e68a120f454534d99045ea3325f7740ea71260bc315f82e29731d570a6e8"}, 23 | "telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"}, 24 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 25 | } 26 | -------------------------------------------------------------------------------- /test/lib/ses/parser_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExAws.SES.ParserTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias ExAws.SES.Parsers 5 | 6 | defp to_success(doc) do 7 | {:ok, %{body: doc}} 8 | end 9 | 10 | defp to_error(doc) do 11 | {:error, {:http_error, 403, %{body: doc}}} 12 | end 13 | 14 | test "#parse a verify_email_identity response" do 15 | rsp = 16 | """ 17 | 18 | 19 | 20 | d8eb8250-be9b-11e6-b7f7-d570946af758 21 | 22 | 23 | """ 24 | |> to_success 25 | 26 | {:ok, %{body: parsed_doc}} = Parsers.parse(rsp, :verify_email_identity) 27 | assert parsed_doc == %{request_id: "d8eb8250-be9b-11e6-b7f7-d570946af758"} 28 | end 29 | 30 | test "#parse a verify_domain_identity response" do 31 | rsp = 32 | """ 33 | 34 | 35 | u4GmlJ3cPJfxxZbLSPMkLOPjQvJW1HPvA6Pmi21CPIE= 36 | 37 | 38 | d8eb8250-be9b-11e6-b7f7-d570946af758 39 | 40 | 41 | """ 42 | |> to_success 43 | 44 | {:ok, %{body: parsed_doc}} = Parsers.parse(rsp, :verify_domain_identity) 45 | assert parsed_doc == %{request_id: "d8eb8250-be9b-11e6-b7f7-d570946af758", verification_token: "u4GmlJ3cPJfxxZbLSPMkLOPjQvJW1HPvA6Pmi21CPIE="} 46 | end 47 | 48 | test "#parse a verify_domain_dkim response" do 49 | rsp = 50 | """ 51 | 52 | 53 | 54 | 5livxhounddpfqprdog22m4c337ake5o 55 | tbnwx5g3l0zmstwf2c258r36pvpnksbt 56 | bbtl43drumsloilm2zfjlhj3c7v12a5d 57 | 58 | 59 | 60 | d8eb8250-be9b-11e6-b7f7-d570946af758 61 | 62 | 63 | """ 64 | |> to_success 65 | 66 | {:ok, %{body: parsed_doc}} = Parsers.parse(rsp, :verify_domain_dkim) 67 | assert parsed_doc == %{request_id: "d8eb8250-be9b-11e6-b7f7-d570946af758", dkim_tokens: %{members: ["5livxhounddpfqprdog22m4c337ake5o", "tbnwx5g3l0zmstwf2c258r36pvpnksbt", "bbtl43drumsloilm2zfjlhj3c7v12a5d"]}} 68 | end 69 | 70 | 71 | test "#parse identity_verification_attributes" do 72 | rsp = 73 | """ 74 | 75 | 76 | 77 | 78 | example.com 79 | 80 | pwCRTZ8zHIJu+vePnXEa4DJmDyGhjSS8V3TkzzL2jI8= 81 | Pending 82 | 83 | 84 | 85 | user@example.com 86 | 87 | Pending 88 | 89 | 90 | 91 | 92 | 93 | f5e3ef21-bec1-11e6-b618-27019a58dab9 94 | 95 | 96 | """ 97 | |> to_success 98 | 99 | verification_attributes = %{ 100 | "example.com" => %{ 101 | verification_token: "pwCRTZ8zHIJu+vePnXEa4DJmDyGhjSS8V3TkzzL2jI8=", 102 | verification_status: "Pending" 103 | }, 104 | "user@example.com" => %{ 105 | verification_status: "Pending" 106 | } 107 | } 108 | 109 | {:ok, %{body: parsed_doc}} = Parsers.parse(rsp, :get_identity_verification_attributes) 110 | assert parsed_doc[:verification_attributes] == verification_attributes 111 | end 112 | 113 | test "#parse configuration_sets" do 114 | rsp = 115 | """ 116 | 117 | 118 | 119 | 120 | test 121 | 122 | 123 | QUFBQUF 124 | 125 | 126 | c177d6ce-c1b0-11e6-9770-29713cf492ad 127 | 128 | 129 | """ 130 | |> to_success 131 | 132 | configuration_sets = %{ 133 | members: ["test"], 134 | next_token: "QUFBQUF" 135 | } 136 | 137 | {:ok, %{body: parsed_doc}} = Parsers.parse(rsp, :list_configuration_sets) 138 | assert parsed_doc[:configuration_sets] == configuration_sets 139 | end 140 | 141 | test "#parse send_email" do 142 | rsp = 143 | """ 144 | 145 | 146 | 0100015914b22075-7a4e3573-ca72-41ce-8eda-388f81232ad9-000000 147 | 148 | 149 | 8194094b-c58a-11e6-b49d-838795cc7d3f 150 | 151 | 152 | """ 153 | |> to_success 154 | 155 | {:ok, %{body: parsed_doc}} = Parsers.parse(rsp, :send_email) 156 | 157 | assert parsed_doc == %{ 158 | request_id: "8194094b-c58a-11e6-b49d-838795cc7d3f", 159 | message_id: "0100015914b22075-7a4e3573-ca72-41ce-8eda-388f81232ad9-000000" 160 | } 161 | end 162 | 163 | test "#parse send_templated_email" do 164 | rsp = 165 | """ 166 | 167 | 168 | 0100015914b22075-7a4e3573-ca72-41ce-8eda-388f81232ad9-000000 169 | 170 | 171 | 8194094b-c58a-11e6-b49d-838795cc7d3f 172 | 173 | 174 | """ 175 | |> to_success 176 | 177 | {:ok, %{body: parsed_doc}} = Parsers.parse(rsp, :send_templated_email) 178 | 179 | assert parsed_doc == %{ 180 | request_id: "8194094b-c58a-11e6-b49d-838795cc7d3f", 181 | message_id: "0100015914b22075-7a4e3573-ca72-41ce-8eda-388f81232ad9-000000" 182 | } 183 | end 184 | 185 | test "#parse send_bulk_templated_email" do 186 | rsp = 187 | """ 188 | 189 | 190 | 191 | 192 | 110000663377dd66-44ffaaaa-11bb-44dd-aa77-998899883388-000000 193 | Success 194 | 195 | 196 | 110000663377dd66-778888dd-cc99-44aa-aa99-bbdd99ddff88-000000 197 | Success 198 | 199 | 200 | 201 | 202 | 22ff88dd-cc11-11ee-99bb-5599ee1144dd 203 | 204 | " 205 | """ 206 | |> to_success 207 | 208 | {:ok, %{body: parsed_doc}} = Parsers.parse(rsp, :send_bulk_templated_email) 209 | 210 | assert parsed_doc == %{ 211 | request_id: "22ff88dd-cc11-11ee-99bb-5599ee1144dd", 212 | messages: [ 213 | %{message_id: "110000663377dd66-44ffaaaa-11bb-44dd-aa77-998899883388-000000", status: "Success"}, 214 | %{message_id: "110000663377dd66-778888dd-cc99-44aa-aa99-bbdd99ddff88-000000", status: "Success"} 215 | ] 216 | } 217 | end 218 | 219 | test "#parse send_raw_email" do 220 | rsp = 221 | """ 222 | 223 | 224 | 0101018264278cea-406a0406-5f46-412b-8f32-05ef31a62aa0-000000 225 | 226 | 227 | 3c2ddfd4-ff21-4a3d-af5f-97ec74811e22 228 | 229 | 230 | """ 231 | |> to_success() 232 | 233 | {:ok, %{body: parsed_doc}} = Parsers.parse(rsp, :send_raw_email) 234 | assert parsed_doc == %{ 235 | request_id: "3c2ddfd4-ff21-4a3d-af5f-97ec74811e22", 236 | message_id: "0101018264278cea-406a0406-5f46-412b-8f32-05ef31a62aa0-000000" 237 | } 238 | end 239 | 240 | test "#parse a delete_identity response" do 241 | rsp = 242 | """ 243 | 244 | 245 | 246 | 88c79dfb-1472-11e7-94c4-4d1ecf50b91f 247 | 248 | 249 | """ 250 | |> to_success 251 | 252 | {:ok, %{body: parsed_doc}} = Parsers.parse(rsp, :delete_identity) 253 | assert parsed_doc == %{request_id: "88c79dfb-1472-11e7-94c4-4d1ecf50b91f"} 254 | end 255 | 256 | test "#parse a set_identity_notification_topic response" do 257 | rsp = 258 | """ 259 | 260 | 261 | 262 | 3d3f811a-1484-11e7-b9b1-db4762b6c4db 263 | 264 | 265 | """ 266 | |> to_success 267 | 268 | {:ok, %{body: parsed_doc}} = Parsers.parse(rsp, :set_identity_notification_topic) 269 | assert parsed_doc == %{request_id: "3d3f811a-1484-11e7-b9b1-db4762b6c4db"} 270 | end 271 | 272 | test "#parse a set_identity_feedback_forwarding_enabled response" do 273 | rsp = 274 | """ 275 | 276 | 277 | 278 | f1cc8133-149a-11e7-91a5-ed1259cbd185 279 | 280 | 281 | """ 282 | |> to_success 283 | 284 | {:ok, %{body: parsed_doc}} = Parsers.parse(rsp, :set_identity_feedback_forwarding_enabled) 285 | assert parsed_doc == %{request_id: "f1cc8133-149a-11e7-91a5-ed1259cbd185"} 286 | end 287 | 288 | test "#parse a set_identity_headers_in_notifications_enabled response" do 289 | rsp = 290 | """ 291 | 292 | 293 | 294 | 01b49b78-30ca-11e7-948a-399bafb173a2 295 | 296 | " 297 | """ 298 | |> to_success 299 | 300 | {:ok, %{body: parsed_doc}} = Parsers.parse(rsp, :set_identity_headers_in_notifications_enabled) 301 | assert parsed_doc == %{request_id: "01b49b78-30ca-11e7-948a-399bafb173a2"} 302 | end 303 | 304 | test "#parse create_template" do 305 | rsp = 306 | """ 307 | 308 | 309 | 310 | 9876defg-c666-111e-88aa-ee8833eeffaa 311 | 312 | 313 | """ 314 | |> to_success 315 | 316 | {:ok, %{body: parsed_doc}} = Parsers.parse(rsp, :create_template) 317 | assert parsed_doc == %{request_id: "9876defg-c666-111e-88aa-ee8833eeffaa"} 318 | end 319 | 320 | test "#parse delete_template" do 321 | rsp = 322 | """ 323 | 324 | 325 | 326 | 12345abcd-c666-111e-88aa-cc8899bb1177 327 | 328 | 329 | """ 330 | |> to_success 331 | 332 | {:ok, %{body: parsed_doc}} = Parsers.parse(rsp, :delete_template) 333 | assert parsed_doc == %{request_id: "12345abcd-c666-111e-88aa-cc8899bb1177"} 334 | end 335 | 336 | test "#parse list_identities" do 337 | rsp = 338 | """ 339 | 340 | 341 | 342 | user@example.com 343 | user2@example.com 344 | 345 | 346 | 347 | 12345abcd-c666-111e-88aa-cc8899bb1177 348 | 349 | 350 | """ 351 | |> to_success 352 | 353 | {:ok, %{body: parsed_doc}} = Parsers.parse(rsp, :list_identities) 354 | 355 | assert parsed_doc == %{ 356 | request_id: "12345abcd-c666-111e-88aa-cc8899bb1177", 357 | custom_verification_email_templates: %{ 358 | members: ["user@example.com", "user2@example.com"], 359 | next_token: "" 360 | }, 361 | identities: %{ 362 | members: ["user@example.com", "user2@example.com"], 363 | next_token: "" 364 | } 365 | } 366 | end 367 | 368 | test "#parse list_custom_verification_email_templates" do 369 | rsp = 370 | """ 371 | 372 | 373 | 374 | 375 | Subject 376 | https://example.com/failure 377 | https://example.com/success 378 | user@example.com 379 | Template Name 380 | 381 | 382 | 383 | 384 | 12345abcd-c666-111e-88aa-cc8899bb1177 385 | 386 | 387 | """ 388 | |> to_success() 389 | 390 | {:ok, %{body: parsed_doc}} = Parsers.parse(rsp, :list_custom_verification_email_templates) 391 | 392 | assert parsed_doc == %{ 393 | request_id: "12345abcd-c666-111e-88aa-cc8899bb1177", 394 | custom_verification_email_templates: %{ 395 | members: [ 396 | %{ 397 | template_name: "Template Name", 398 | template_subject: "Subject", 399 | failure_redirection_url: "https://example.com/failure", 400 | success_redirection_url: "https://example.com/success", 401 | from_email_address: "user@example.com" 402 | } 403 | ], 404 | next_token: "" 405 | } 406 | } 407 | end 408 | 409 | test "#parse error" do 410 | rsp = 411 | """ 412 | 413 | 414 | Sender 415 | MalformedInput 416 | Top level element may not be treated as a list 417 | 418 | 3ac0a9e8-bebd-11e6-9ec4-e5c47e708fa8 419 | 420 | """ 421 | |> to_error 422 | 423 | {:error, {:http_error, 403, err}} = Parsers.parse(rsp, :get_identity_verification_attributes) 424 | 425 | assert "Sender" == err[:type] 426 | assert "MalformedInput" == err[:code] 427 | assert "Top level element may not be treated as a list" == err[:message] 428 | assert "3ac0a9e8-bebd-11e6-9ec4-e5c47e708fa8" == err[:request_id] 429 | end 430 | end 431 | -------------------------------------------------------------------------------- /test/lib/ses_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExAws.SESTest do 2 | use ExUnit.Case, async: true 3 | alias ExAws.SES 4 | 5 | @list_name "test_list" 6 | 7 | setup_all do 8 | {:ok, 9 | email: "user@example.com", 10 | domain: "example.com", 11 | tag: %{Key: "environment", Value: "test"}, 12 | topic: %{TopicName: "test_topic", SubscriptionStatus: "OPT_IN"}, 13 | list_management: %{ContactListName: @list_name, TopicName: "test_topic"}, 14 | destination: %{ 15 | ToAddresses: ["test1@example.com"], 16 | CcAddresses: ["test2@example.com"], 17 | BccAddresses: ["test3@example.com"] 18 | }} 19 | end 20 | 21 | test "#verify_email_identity", ctx do 22 | expected = %{"Action" => "VerifyEmailIdentity", "EmailAddress" => ctx.email} 23 | assert expected == SES.verify_email_identity(ctx.email).params 24 | end 25 | 26 | test "#verify_domain_identity", ctx do 27 | expected = %{"Action" => "VerifyDomainIdentity", "Domain" => ctx.domain} 28 | assert expected == SES.verify_domain_identity(ctx.domain).params 29 | end 30 | 31 | test "#verify_domain_dkim", ctx do 32 | expected = %{"Action" => "VerifyDomainDkim", "Domain" => ctx.domain} 33 | assert expected == SES.verify_domain_dkim(ctx.domain).params 34 | end 35 | 36 | @tag :integration 37 | test "#verify_email_identity request" do 38 | resp = SES.verify_email_identity("success@simulator.amazonses.com") |> ExAws.request() 39 | assert {:ok, %{body: %{request_id: _}}} = resp 40 | end 41 | 42 | @tag :integration 43 | test "#verify_domain_identity request" do 44 | resp = SES.verify_domain_identity("simulator.amazonses.com") |> ExAws.request() 45 | assert {:ok, %{body: %{request_id: _}}} = resp 46 | end 47 | 48 | @tag :integration 49 | test "#verify_domain_dkimrequest" do 50 | resp = SES.verify_domain_dkim("simulator.amazonses.com") |> ExAws.request() 51 | assert {:ok, %{body: %{request_id: _}}} = resp 52 | end 53 | 54 | test "#identity_verification_attributes", ctx do 55 | expected = %{ 56 | "Action" => "GetIdentityVerificationAttributes", 57 | "Identities.member.1" => ctx.email 58 | } 59 | 60 | assert expected == SES.get_identity_verification_attributes([ctx.email]).params 61 | end 62 | 63 | test "#configuration_sets" do 64 | expected = %{"Action" => "ListConfigurationSets", "MaxItems" => 1, "NextToken" => "QUFBQUF"} 65 | assert expected == SES.list_configuration_sets(max_items: 1, next_token: "QUFBQUF").params 66 | end 67 | 68 | test "#list_identities" do 69 | expected = %{"Action" => "ListIdentities", "MaxItems" => 1, "NextToken" => "QUFBQUF", "IdentityType" => "Domain"} 70 | assert expected == SES.list_identities(max_items: 1, next_token: "QUFBQUF", identity_type: "Domain").params 71 | 72 | expected = %{"Action" => "ListIdentities", "MaxItems" => 1, "NextToken" => "QUFBQUF"} 73 | assert expected == SES.list_identities(max_items: 1, next_token: "QUFBQUF").params 74 | end 75 | 76 | describe "contact lists" do 77 | test "#create_contact_list" do 78 | tag = %{Key: "environment", Value: "test"} 79 | 80 | expected_data = %{ 81 | "ContactListName" => @list_name, 82 | "Tags" => [%{Key: "environment", Value: "test"}] 83 | } 84 | 85 | operation = SES.create_contact_list(@list_name, tags: [tag]) 86 | 87 | assert operation.http_method == :post 88 | assert operation.path == "/v2/email/contact-lists" 89 | assert operation.data == expected_data 90 | end 91 | 92 | test "#update_contact_list" do 93 | description = "test description" 94 | topic = %{TopicName: "test_topic", DisplayName: "test", DefaultSubscriptionStatus: "OPT_OUT"} 95 | 96 | expected_data = %{ 97 | "ContactListName" => @list_name, 98 | "Description" => description, 99 | "Topics" => [topic] 100 | } 101 | 102 | operation = SES.update_contact_list(@list_name, description: description, topics: [topic]) 103 | 104 | assert operation.http_method == :put 105 | assert operation.path == "/v2/email/contact-lists/#{@list_name}" 106 | assert operation.data == expected_data 107 | end 108 | 109 | test "#list_contact_lists" do 110 | operation = SES.list_contact_lists() 111 | 112 | assert operation.http_method == :get 113 | assert operation.path == "/v2/email/contact-lists" 114 | end 115 | 116 | test "#get_contact_list" do 117 | operation = SES.get_contact_list(@list_name) 118 | 119 | assert operation.http_method == :get 120 | assert operation.path == "/v2/email/contact-lists/#{@list_name}" 121 | end 122 | 123 | test "#delete_contact_list" do 124 | operation = SES.delete_contact_list(@list_name) 125 | 126 | assert operation.http_method == :delete 127 | assert operation.path == "/v2/email/contact-lists/#{@list_name}" 128 | end 129 | 130 | test "#create_import_job" do 131 | source = %{DataFormat: "CSV", S3Url: "s3://test_bucket/test_object.csv"} 132 | destination = %{ContactListDestination: %{ContactListImportAction: "PUT", ContactListName: @list_name}} 133 | 134 | expected_data = %{ 135 | ImportDataSource: source, 136 | ImportDestination: destination 137 | } 138 | 139 | operation = SES.create_import_job(source, destination) 140 | 141 | assert operation.http_method == :post 142 | assert operation.path == "/v2/email/import-jobs" 143 | assert operation.data == expected_data 144 | end 145 | end 146 | 147 | describe "contacts" do 148 | test "#create_contact" do 149 | email = "test@example.com" 150 | topic = %{TopicName: "test_topic", SubscriptionStatus: "OPT_IN"} 151 | attributes = "test attribute" 152 | unsubscribe = false 153 | 154 | expected_data = %{ 155 | "EmailAddress" => email, 156 | "TopicPreferences" => [topic], 157 | "AttributesData" => attributes, 158 | "UnsubscribeAll" => unsubscribe 159 | } 160 | 161 | operation = 162 | SES.create_contact( 163 | @list_name, 164 | email, 165 | attributes: attributes, 166 | topic_preferences: [topic], 167 | unsubscribe_all: unsubscribe 168 | ) 169 | 170 | assert operation.http_method == :post 171 | assert operation.path == "/v2/email/contact-lists/#{@list_name}/contacts" 172 | assert operation.data == expected_data 173 | end 174 | 175 | test "#update_contact" do 176 | email = "test+bar@example.com" 177 | topic = %{TopicName: "test_topic", SubscriptionStatus: "OPT_IN"} 178 | attributes = "test attribute" 179 | unsubscribe = false 180 | 181 | expected_data = %{ 182 | "TopicPreferences" => [topic], 183 | "AttributesData" => attributes, 184 | "UnsubscribeAll" => unsubscribe 185 | } 186 | 187 | operation = 188 | SES.update_contact( 189 | @list_name, 190 | email, 191 | attributes: attributes, 192 | topic_preferences: [topic], 193 | unsubscribe_all: unsubscribe 194 | ) 195 | 196 | assert operation.http_method == :put 197 | assert operation.path == "/v2/email/contact-lists/#{@list_name}/contacts/test%2Bbar%40example.com" 198 | assert operation.data == expected_data 199 | end 200 | 201 | test "#list_contacts" do 202 | operation = SES.list_contacts(@list_name) 203 | 204 | assert operation.http_method == :get 205 | assert operation.path == "/v2/email/contact-lists/#{@list_name}/contacts" 206 | end 207 | 208 | test "#get_contact" do 209 | email = "test+bar@example.com" 210 | operation = SES.get_contact(@list_name, email) 211 | 212 | assert operation.http_method == :get 213 | assert operation.path == "/v2/email/contact-lists/#{@list_name}/contacts/test%2Bbar%40example.com" 214 | end 215 | 216 | test "#delete_contact" do 217 | email = "test+bar@example.com" 218 | operation = SES.delete_contact(@list_name, email) 219 | 220 | assert operation.http_method == :delete 221 | assert operation.path == "/v2/email/contact-lists/#{@list_name}/contacts/test%2Bbar%40example.com" 222 | end 223 | end 224 | 225 | describe "suppressed destinations" do 226 | test "#put_suppressed_destination" do 227 | email = "test@example.com" 228 | operation = SES.put_suppressed_destination(email, :BOUNCE) 229 | 230 | expected_data = %{ 231 | EmailAddress: email, 232 | Reason: :BOUNCE 233 | } 234 | 235 | assert operation.http_method == :put 236 | assert operation.path == "/v2/email/suppression/addresses" 237 | assert operation.data == expected_data 238 | end 239 | 240 | test "#delete_suppressed_destination" do 241 | email = "test+bar@example.com" 242 | operation = SES.delete_suppressed_destination(email) 243 | 244 | assert operation.http_method == :delete 245 | assert operation.path == "/v2/email/suppression/addresses/test%2Bbar%40example.com" 246 | end 247 | end 248 | 249 | describe "v2 API send_email" do 250 | test "simple html", context do 251 | content = %{ 252 | Simple: %{ 253 | Body: %{ 254 | Html: %{ 255 | Data: "test email via elixir ses" 256 | } 257 | }, 258 | Subject: %{Data: "test email via elixir ses"} 259 | } 260 | } 261 | 262 | expected_data = %{ 263 | Content: content, 264 | Destination: context[:destination], 265 | EmailTags: [context[:tag]], 266 | FromEmailAddress: context[:email], 267 | ListManagementOptions: context[:list_management] 268 | } 269 | 270 | operation = 271 | SES.send_email_v2( 272 | context[:destination], 273 | content, 274 | context[:email], 275 | tags: [context[:tag]], 276 | list_management: context[:list_management] 277 | ) 278 | 279 | assert operation.http_method == :post 280 | assert operation.path == "/v2/email/outbound-emails" 281 | assert operation.data == expected_data 282 | end 283 | 284 | test "simple text", context do 285 | content = %{ 286 | Simple: %{ 287 | Body: %{ 288 | Text: %{ 289 | Data: "test email via elixir ses" 290 | } 291 | }, 292 | Subject: %{Data: "test email via elixir ses"} 293 | } 294 | } 295 | 296 | expected_data = %{ 297 | Content: content, 298 | Destination: context[:destination], 299 | EmailTags: [context[:tag]], 300 | FromEmailAddress: context[:email], 301 | ListManagementOptions: context[:list_management] 302 | } 303 | 304 | operation = 305 | SES.send_email_v2( 306 | context[:destination], 307 | content, 308 | context[:email], 309 | tags: [context[:tag]], 310 | list_management: context[:list_management] 311 | ) 312 | 313 | assert operation.http_method == :post 314 | assert operation.path == "/v2/email/outbound-emails" 315 | assert operation.data == expected_data 316 | end 317 | end 318 | 319 | describe "#send_email" do 320 | test "with required params only" do 321 | dst = %{to: ["success@simulator.amazonses.com"]} 322 | msg = %{body: %{}, subject: %{data: "subject"}} 323 | 324 | expected = %{ 325 | "Action" => "SendEmail", 326 | "Destination.ToAddresses.member.1" => "success@simulator.amazonses.com", 327 | "Message.Subject.Data" => "subject", 328 | "Source" => "user@example.com" 329 | } 330 | 331 | assert expected == SES.send_email(dst, msg, "user@example.com").params 332 | end 333 | 334 | test "with all optional params" do 335 | dst = %{ 336 | bcc: ["success@simulator.amazonses.com"], 337 | cc: ["success@simulator.amazonses.com"], 338 | to: ["success@simulator.amazonses.com", "bounce@simulator.amazonses.com"] 339 | } 340 | 341 | msg = SES.build_message("html", "text", "subject") 342 | 343 | expected = %{ 344 | "Action" => "SendEmail", 345 | "ConfigurationSetName" => "test", 346 | "Destination.ToAddresses.member.1" => "success@simulator.amazonses.com", 347 | "Destination.ToAddresses.member.2" => "bounce@simulator.amazonses.com", 348 | "Destination.CcAddresses.member.1" => "success@simulator.amazonses.com", 349 | "Destination.BccAddresses.member.1" => "success@simulator.amazonses.com", 350 | "Message.Body.Html.Data" => "html", 351 | "Message.Body.Html.Charset" => "UTF-8", 352 | "Message.Body.Text.Data" => "text", 353 | "Message.Body.Text.Charset" => "UTF-8", 354 | "Message.Subject.Data" => "subject", 355 | "Message.Subject.Charset" => "UTF-8", 356 | "ReplyToAddresses.member.1" => "user@example.com", 357 | "ReplyToAddresses.member.2" => "user1@example.com", 358 | "ReturnPath" => "feedback@example.com", 359 | "ReturnPathArn" => "arn:aws:ses:us-east-1:123456789012:identity/example.com", 360 | "Source" => "user@example.com", 361 | "SourceArn" => "east-1:123456789012:identity/example.com", 362 | "Tags.member.1.Name" => "tag1", 363 | "Tags.member.1.Value" => "tag1value1", 364 | "Tags.member.2.Name" => "tag2", 365 | "Tags.member.2.Value" => "tag2value1" 366 | } 367 | 368 | assert expected == 369 | SES.send_email( 370 | dst, 371 | msg, 372 | "user@example.com", 373 | configuration_set_name: "test", 374 | return_path: "feedback@example.com", 375 | return_path_arn: "arn:aws:ses:us-east-1:123456789012:identity/example.com", 376 | source_arn: "east-1:123456789012:identity/example.com", 377 | reply_to: ["user@example.com", "user1@example.com"], 378 | tags: [%{name: "tag1", value: "tag1value1"}, %{name: "tag2", value: "tag2value1"}] 379 | ).params 380 | end 381 | end 382 | 383 | describe "#send_raw_email" do 384 | setup do 385 | %{ 386 | raw_email: 387 | "To: alice@example.com\r\nSubject: =?utf-8?Q?Welcome to the app.?=\r\nReply-To: chuck@example.com\r\nMime-Version: 1.0\r\nFrom: bob@example.com\r\nContent-Type: multipart/alternative; boundary=\"9081958709C029F90BFFF130\"\r\nCc: john@example.com\r\nBcc: jane@example.com\r\n\r\n--9081958709C029F90BFFF130\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nThanks for joining!\r\n\r\n--9081958709C029F90BFFF130\r\nContent-Type: text/html\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nThanks for joining!\r\n--9081958709C029F90BFFF130--", 388 | raw_email_data: 389 | "VG86IGFsaWNlQGV4YW1wbGUuY29tDQpTdWJqZWN0OiA9P3V0Zi04P1E/V2VsY29tZSB0byB0aGUgYXBwLj89DQpSZXBseS1UbzogY2h1Y2tAZXhhbXBsZS5jb20NCk1pbWUtVmVyc2lvbjogMS4wDQpGcm9tOiBib2JAZXhhbXBsZS5jb20NCkNvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L2FsdGVybmF0aXZlOyBib3VuZGFyeT0iOTA4MTk1ODcwOUMwMjlGOTBCRkZGMTMwIg0KQ2M6IGpvaG5AZXhhbXBsZS5jb20NCkJjYzogamFuZUBleGFtcGxlLmNvbQ0KDQotLTkwODE5NTg3MDlDMDI5RjkwQkZGRjEzMA0KQ29udGVudC1UeXBlOiB0ZXh0L3BsYWluDQpDb250ZW50LVRyYW5zZmVyLUVuY29kaW5nOiBxdW90ZWQtcHJpbnRhYmxlDQoNClRoYW5rcyBmb3Igam9pbmluZyENCg0KLS05MDgxOTU4NzA5QzAyOUY5MEJGRkYxMzANCkNvbnRlbnQtVHlwZTogdGV4dC9odG1sDQpDb250ZW50LVRyYW5zZmVyLUVuY29kaW5nOiBxdW90ZWQtcHJpbnRhYmxlDQoNCjxzdHJvbmc+VGhhbmtzIGZvciBqb2luaW5nITwvc3Ryb25nPg0KLS05MDgxOTU4NzA5QzAyOUY5MEJGRkYxMzAtLQ==" 390 | } 391 | end 392 | 393 | test "with required params only", %{raw_email: msg, raw_email_data: data} do 394 | expected = %{ 395 | "Action" => "SendRawEmail", 396 | "RawMessage.Data" => data 397 | } 398 | 399 | assert expected == SES.send_raw_email(msg).params 400 | end 401 | 402 | test "with all optional params", %{raw_email: msg, raw_email_data: data} do 403 | expected = %{ 404 | "Action" => "SendRawEmail", 405 | "ConfigurationSetName" => "test", 406 | "FromArn" => "east-1:123456789012:identity/example.com", 407 | "ReturnPathArn" => "arn:aws:ses:us-east-1:123456789012:identity/example.com", 408 | "Source" => "bob@example.com", 409 | "SourceArn" => "east-1:123456789012:identity/example.com", 410 | "Tags.member.1.Name" => "tag1", 411 | "Tags.member.1.Value" => "tag1value1", 412 | "Tags.member.2.Name" => "tag2", 413 | "Tags.member.2.Value" => "tag2value1", 414 | "RawMessage.Data" => data 415 | } 416 | 417 | assert expected == 418 | SES.send_raw_email(msg, 419 | configuration_set_name: "test", 420 | from_arn: "east-1:123456789012:identity/example.com", 421 | return_path_arn: "arn:aws:ses:us-east-1:123456789012:identity/example.com", 422 | source: "bob@example.com", 423 | source_arn: "east-1:123456789012:identity/example.com", 424 | tags: [%{name: "tag1", value: "tag1value1"}, %{name: "tag2", value: "tag2value1"}] 425 | ).params 426 | end 427 | end 428 | 429 | describe "#send_templated_email" do 430 | test "with required params only" do 431 | dst = %{to: ["success@simulator.amazonses.com"]} 432 | src = "user@example.com" 433 | template_data = %{data1: "data1", data2: "data2"} 434 | 435 | expected = %{ 436 | "Action" => "SendTemplatedEmail", 437 | "Destination.ToAddresses.member.1" => "success@simulator.amazonses.com", 438 | "Template" => "my_template", 439 | "Source" => "user@example.com", 440 | "TemplateData" => Jason.encode!(template_data) 441 | } 442 | 443 | assert expected == SES.send_templated_email(dst, src, "my_template", template_data).params 444 | end 445 | 446 | test "with all optional params" do 447 | dst = %{ 448 | bcc: ["success@simulator.amazonses.com"], 449 | cc: ["success@simulator.amazonses.com"], 450 | to: ["success@simulator.amazonses.com", "bounce@simulator.amazonses.com"] 451 | } 452 | 453 | src = "user@example.com" 454 | template_data = %{data1: "data1", data2: "data2"} 455 | 456 | expected = %{ 457 | "Action" => "SendTemplatedEmail", 458 | "ConfigurationSetName" => "test", 459 | "Destination.ToAddresses.member.1" => "success@simulator.amazonses.com", 460 | "Destination.ToAddresses.member.2" => "bounce@simulator.amazonses.com", 461 | "Destination.CcAddresses.member.1" => "success@simulator.amazonses.com", 462 | "Destination.BccAddresses.member.1" => "success@simulator.amazonses.com", 463 | "ReplyToAddresses.member.1" => "user@example.com", 464 | "ReplyToAddresses.member.2" => "user1@example.com", 465 | "ReturnPath" => "feedback@example.com", 466 | "ReturnPathArn" => "arn:aws:ses:us-east-1:123456789012:identity/example.com", 467 | "Source" => "user@example.com", 468 | "SourceArn" => "east-1:123456789012:identity/example.com", 469 | "Tags.member.1.Name" => "tag1", 470 | "Tags.member.1.Value" => "tag1value1", 471 | "Tags.member.2.Name" => "tag2", 472 | "Tags.member.2.Value" => "tag2value1", 473 | "Template" => "my_template", 474 | "TemplateData" => Jason.encode!(template_data) 475 | } 476 | 477 | assert expected == 478 | SES.send_templated_email( 479 | dst, 480 | src, 481 | "my_template", 482 | template_data, 483 | configuration_set_name: "test", 484 | return_path: "feedback@example.com", 485 | return_path_arn: "arn:aws:ses:us-east-1:123456789012:identity/example.com", 486 | source_arn: "east-1:123456789012:identity/example.com", 487 | reply_to: ["user@example.com", "user1@example.com"], 488 | tags: [%{name: "tag1", value: "tag1value1"}, %{name: "tag2", value: "tag2value1"}] 489 | ).params 490 | end 491 | end 492 | 493 | describe "#send_bulk_templated_email" do 494 | test "with required params only" do 495 | template = "my_template" 496 | source = "user@example.com" 497 | 498 | replacement_template_data1 = %{data1: "value1"} 499 | replacement_template_data2 = %{data1: "value2"} 500 | 501 | destinations = [ 502 | %{ 503 | destination: %{to: ["email1@email.com", "email2@email.com"]}, 504 | replacement_template_data: replacement_template_data1 505 | }, 506 | %{ 507 | destination: %{ 508 | to: ["email3@email.com"], 509 | cc: ["email4@email.com", "email5@email.com"], 510 | bcc: ["email6@email.com", "email7@email.com"] 511 | }, 512 | replacement_template_data: replacement_template_data2 513 | }, 514 | %{destination: %{to: ["email8@email.com"]}} 515 | ] 516 | 517 | expected = %{ 518 | "Action" => "SendBulkTemplatedEmail", 519 | "Template" => "my_template", 520 | "Source" => "user@example.com", 521 | "Destinations.member.1.Destination.ToAddresses.member.1" => "email1@email.com", 522 | "Destinations.member.1.Destination.ToAddresses.member.2" => "email2@email.com", 523 | "Destinations.member.1.ReplacementTemplateData" => Jason.encode!(replacement_template_data1), 524 | "Destinations.member.2.Destination.ToAddresses.member.1" => "email3@email.com", 525 | "Destinations.member.2.Destination.CcAddresses.member.1" => "email4@email.com", 526 | "Destinations.member.2.Destination.CcAddresses.member.2" => "email5@email.com", 527 | "Destinations.member.2.Destination.BccAddresses.member.1" => "email6@email.com", 528 | "Destinations.member.2.Destination.BccAddresses.member.2" => "email7@email.com", 529 | "Destinations.member.2.ReplacementTemplateData" => Jason.encode!(replacement_template_data2), 530 | "Destinations.member.3.Destination.ToAddresses.member.1" => "email8@email.com", 531 | "DefaultTemplateData" => "{}" 532 | } 533 | 534 | assert expected == SES.send_bulk_templated_email(template, source, destinations).params 535 | end 536 | 537 | test "with all optional params" do 538 | template = "my_template" 539 | source = "user@example.com" 540 | 541 | replacement_template_data1 = %{data1: "value1"} 542 | replacement_template_data2 = %{data1: "value2"} 543 | default_template_data = %{data1: "DefaultValue"} 544 | 545 | destinations = [ 546 | %{ 547 | destination: %{to: ["email1@email.com", "email2@email.com"]}, 548 | replacement_template_data: replacement_template_data1 549 | }, 550 | %{ 551 | destination: %{ 552 | to: ["email3@email.com"], 553 | cc: ["email4@email.com", "email5@email.com"], 554 | bcc: ["email6@email.com", "email7@email.com"] 555 | }, 556 | replacement_template_data: replacement_template_data2 557 | }, 558 | %{destination: %{to: ["email8@email.com"]}} 559 | ] 560 | 561 | expected = %{ 562 | "Action" => "SendBulkTemplatedEmail", 563 | "ConfigurationSetName" => "test", 564 | "Template" => "my_template", 565 | "Source" => "user@example.com", 566 | "Destinations.member.1.Destination.ToAddresses.member.1" => "email1@email.com", 567 | "Destinations.member.1.Destination.ToAddresses.member.2" => "email2@email.com", 568 | "Destinations.member.1.ReplacementTemplateData" => Jason.encode!(replacement_template_data1), 569 | "Destinations.member.2.Destination.ToAddresses.member.1" => "email3@email.com", 570 | "Destinations.member.2.Destination.CcAddresses.member.1" => "email4@email.com", 571 | "Destinations.member.2.Destination.CcAddresses.member.2" => "email5@email.com", 572 | "Destinations.member.2.Destination.BccAddresses.member.1" => "email6@email.com", 573 | "Destinations.member.2.Destination.BccAddresses.member.2" => "email7@email.com", 574 | "Destinations.member.2.ReplacementTemplateData" => Jason.encode!(replacement_template_data2), 575 | "Destinations.member.3.Destination.ToAddresses.member.1" => "email8@email.com", 576 | "DefaultTemplateData" => Jason.encode!(default_template_data), 577 | "ReplyToAddresses.member.1" => "user@example.com", 578 | "ReplyToAddresses.member.2" => "user1@example.com", 579 | "ReturnPath" => "feedback@example.com", 580 | "ReturnPathArn" => "arn:aws:ses:us-east-1:123456789012:identity/example.com", 581 | "SourceArn" => "east-1:123456789012:identity/example.com", 582 | "Tags.member.1.Name" => "tag1", 583 | "Tags.member.1.Value" => "tag1value1", 584 | "Tags.member.2.Name" => "tag2", 585 | "Tags.member.2.Value" => "tag2value1" 586 | } 587 | 588 | assert expected == 589 | SES.send_bulk_templated_email( 590 | template, 591 | source, 592 | destinations, 593 | default_template_data: default_template_data, 594 | configuration_set_name: "test", 595 | return_path: "feedback@example.com", 596 | return_path_arn: "arn:aws:ses:us-east-1:123456789012:identity/example.com", 597 | source_arn: "east-1:123456789012:identity/example.com", 598 | reply_to: ["user@example.com", "user1@example.com"], 599 | tags: [%{name: "tag1", value: "tag1value1"}, %{name: "tag2", value: "tag2value1"}] 600 | ).params 601 | end 602 | end 603 | 604 | test "#delete_identity", ctx do 605 | expected = %{"Action" => "DeleteIdentity", "Identity" => ctx.email} 606 | assert expected == SES.delete_identity(ctx.email).params 607 | end 608 | 609 | describe "#set_identity_notification_topic" do 610 | test "accepts correct notification types", ctx do 611 | Enum.each([:bounce, :complaint, :delivery], fn type -> 612 | notification_type = Atom.to_string(type) |> String.capitalize() 613 | 614 | expected = %{ 615 | "Action" => "SetIdentityNotificationTopic", 616 | "Identity" => ctx.email, 617 | "NotificationType" => notification_type 618 | } 619 | 620 | assert expected == SES.set_identity_notification_topic(ctx.email, type).params 621 | end) 622 | end 623 | 624 | test "optional params", ctx do 625 | sns_topic_arn = "arn:aws:sns:us-east-1:123456789012:my_corporate_topic:02034b43-fefa-4e07-a5eb-3be56f8c54ce" 626 | 627 | expected = %{ 628 | "Action" => "SetIdentityNotificationTopic", 629 | "Identity" => ctx.email, 630 | "NotificationType" => "Bounce", 631 | "SnsTopic" => sns_topic_arn 632 | } 633 | 634 | assert expected == SES.set_identity_notification_topic(ctx.email, :bounce, sns_topic: sns_topic_arn).params 635 | end 636 | end 637 | 638 | test "#set_identity_feedback_forwarding_enabled", ctx do 639 | enabled = true 640 | 641 | expected = %{ 642 | "Action" => "SetIdentityFeedbackForwardingEnabled", 643 | "ForwardingEnabled" => enabled, 644 | "Identity" => ctx.email 645 | } 646 | 647 | assert expected == SES.set_identity_feedback_forwarding_enabled(enabled, ctx.email).params 648 | end 649 | 650 | test "#set_identity_headers_in_notifications_enabled", ctx do 651 | enabled = true 652 | 653 | expected = %{ 654 | "Action" => "SetIdentityHeadersInNotificationsEnabled", 655 | "Identity" => ctx.email, 656 | "Enabled" => enabled, 657 | "NotificationType" => "Delivery" 658 | } 659 | 660 | assert expected == SES.set_identity_headers_in_notifications_enabled(ctx.email, :delivery, enabled).params 661 | end 662 | 663 | test "#get_template" do 664 | templateName = "MyTemplate" 665 | 666 | expected = %{ 667 | "Action" => "GetTemplate", 668 | "TemplateName" => templateName 669 | } 670 | 671 | assert expected == SES.get_template(templateName).params 672 | end 673 | 674 | test "#list_templates" do 675 | expected = %{ 676 | "Action" => "ListTemplates", 677 | "MaxItems" => 1, 678 | "NextToken" => "QUFBQUF" 679 | } 680 | 681 | assert expected == SES.list_templates(max_items: 1, next_token: "QUFBQUF").params 682 | end 683 | 684 | describe "#create_template" do 685 | test "with required params only" do 686 | templateName = "MyTemplate" 687 | subject = "Greetings, {{name}}!" 688 | html = "

Hello {{name}},

Your favorite animal is {{favoriteanimal}}.

" 689 | text = "Dear {{name}},\r\nYour favorite animal is {{favoriteanimal}}." 690 | 691 | expected = %{ 692 | "Action" => "CreateTemplate", 693 | "Template.TemplateName" => templateName, 694 | "Template.SubjectPart" => subject, 695 | "Template.HtmlPart" => html, 696 | "Template.TextPart" => text 697 | } 698 | 699 | assert expected == SES.create_template(templateName, subject, html, text).params 700 | end 701 | 702 | test "without text part" do 703 | templateName = "MyTemplate" 704 | subject = "Greetings, {{name}}!" 705 | html = "

Hello {{name}},

Your favorite animal is {{favoriteanimal}}.

" 706 | 707 | expected = %{ 708 | "Action" => "CreateTemplate", 709 | "Template.TemplateName" => templateName, 710 | "Template.SubjectPart" => subject, 711 | "Template.HtmlPart" => html 712 | } 713 | 714 | assert expected == SES.create_template(templateName, subject, html, nil).params 715 | end 716 | 717 | test "with all optional params" do 718 | templateName = "MyTemplate" 719 | subject = "Greetings, {{name}}!" 720 | html = "

Hello {{name}},

Your favorite animal is {{favoriteanimal}}.

" 721 | text = "Dear {{name}},\r\nYour favorite animal is {{favoriteanimal}}." 722 | 723 | expected = %{ 724 | "Action" => "CreateTemplate", 725 | "Template.TemplateName" => templateName, 726 | "Template.SubjectPart" => subject, 727 | "Template.HtmlPart" => html, 728 | "Template.TextPart" => text, 729 | "ConfigurationSetName" => "test" 730 | } 731 | 732 | assert expected == SES.create_template(templateName, subject, html, text, configuration_set_name: "test").params 733 | end 734 | end 735 | 736 | describe "#update_template" do 737 | test "with required params only" do 738 | templateName = "MyTemplate" 739 | subject = "Greetings, {{name}}!" 740 | html = "

Hello {{name}},

Your favorite animal is {{favoriteanimal}}.

" 741 | text = "Dear {{name}},\r\nYour favorite animal is {{favoriteanimal}}." 742 | 743 | expected = %{ 744 | "Action" => "UpdateTemplate", 745 | "Template.TemplateName" => templateName, 746 | "Template.SubjectPart" => subject, 747 | "Template.HtmlPart" => html, 748 | "Template.TextPart" => text 749 | } 750 | 751 | assert expected == SES.update_template(templateName, subject, html, text).params 752 | end 753 | 754 | test "without text part" do 755 | templateName = "MyTemplate" 756 | subject = "Greetings, {{name}}!" 757 | html = "

Hello {{name}},

Your favorite animal is {{favoriteanimal}}.

" 758 | 759 | expected = %{ 760 | "Action" => "UpdateTemplate", 761 | "Template.TemplateName" => templateName, 762 | "Template.SubjectPart" => subject, 763 | "Template.HtmlPart" => html 764 | } 765 | 766 | assert expected == SES.update_template(templateName, subject, html, nil).params 767 | end 768 | 769 | test "with all optional params" do 770 | templateName = "MyTemplate" 771 | subject = "Greetings, {{name}}!" 772 | html = "

Hello {{name}},

Your favorite animal is {{favoriteanimal}}.

" 773 | text = "Dear {{name}},\r\nYour favorite animal is {{favoriteanimal}}." 774 | 775 | expected = %{ 776 | "Action" => "UpdateTemplate", 777 | "Template.TemplateName" => templateName, 778 | "Template.SubjectPart" => subject, 779 | "Template.HtmlPart" => html, 780 | "Template.TextPart" => text, 781 | "ConfigurationSetName" => "test" 782 | } 783 | 784 | assert expected == SES.update_template(templateName, subject, html, text, configuration_set_name: "test").params 785 | end 786 | end 787 | 788 | test "#delete_template" do 789 | templateName = "MyTemplate" 790 | 791 | expected = %{ 792 | "Action" => "DeleteTemplate", 793 | "TemplateName" => templateName 794 | } 795 | 796 | assert expected == SES.delete_template(templateName).params 797 | end 798 | 799 | describe "get_email_template/1" do 800 | test "with param" do 801 | templateName = "MyTemplate" 802 | expected = "/v2/email/templates/#{templateName}" 803 | assert expected == SES.get_email_template(templateName).path 804 | end 805 | end 806 | 807 | describe "list_email_templates/1" do 808 | test "with pagination params" do 809 | expected = "/v2/email/templates?NextToken=QUFBQUF&PageSize=1" 810 | assert expected == SES.list_email_templates(page_size: 1, next_token: "QUFBQUF").path 811 | end 812 | 813 | test "without pagination params" do 814 | expected = "/v2/email/templates?" 815 | assert expected == SES.list_email_templates().path 816 | end 817 | end 818 | 819 | describe "create_email_template/4" do 820 | test "with required params only" do 821 | templateName = "MyTemplate" 822 | subject = "Greetings, {{name}}!" 823 | html = "

Hello {{name}},

Your favorite animal is {{favoriteanimal}}.

" 824 | text = "Dear {{name}},\r\nYour favorite animal is {{favoriteanimal}}." 825 | 826 | expected = %{ 827 | "TemplateName" => templateName, 828 | "TemplateContent" => %{ 829 | "Subject" => subject, 830 | "Html" => html, 831 | "Text" => text 832 | } 833 | } 834 | 835 | assert expected == SES.create_email_template(templateName, subject, html, text).data 836 | end 837 | 838 | test "without text part" do 839 | templateName = "MyTemplate" 840 | subject = "Greetings, {{name}}!" 841 | html = "

Hello {{name}},

Your favorite animal is {{favoriteanimal}}.

" 842 | 843 | expected = %{ 844 | "TemplateName" => templateName, 845 | "TemplateContent" => %{ 846 | "Subject" => subject, 847 | "Html" => html 848 | } 849 | } 850 | 851 | assert expected == SES.create_email_template(templateName, subject, html, nil).data 852 | end 853 | end 854 | 855 | describe "update_email_template/4" do 856 | test "with required params only" do 857 | templateName = "MyTemplate" 858 | subject = "Greetings, {{name}}!" 859 | html = "

Hello {{name}},

Your favorite animal is {{favoriteanimal}}.

" 860 | text = "Dear {{name}},\r\nYour favorite animal is {{favoriteanimal}}." 861 | 862 | expected = %{ 863 | "TemplateContent" => %{ 864 | "Subject" => subject, 865 | "Html" => html, 866 | "Text" => text 867 | } 868 | } 869 | 870 | assert expected == SES.update_email_template(templateName, subject, html, text).data 871 | end 872 | 873 | test "without text part" do 874 | templateName = "MyTemplate" 875 | subject = "Greetings, {{name}}!" 876 | html = "

Hello {{name}},

Your favorite animal is {{favoriteanimal}}.

" 877 | 878 | expected = %{ 879 | "TemplateContent" => %{ 880 | "Subject" => subject, 881 | "Html" => html 882 | } 883 | } 884 | 885 | assert expected == SES.update_email_template(templateName, subject, html, nil).data 886 | end 887 | end 888 | 889 | describe "delete_email_template/1" do 890 | test "with required param" do 891 | templateName = "MyTemplate" 892 | expected = "/v2/email/templates/MyTemplate" 893 | assert expected == SES.delete_email_template(templateName).path 894 | end 895 | end 896 | 897 | describe "test_render_email_template/2" do 898 | test "with params" do 899 | template_name = "MyTemplate" 900 | template_data = %{data1: "data1", data2: "data2"} 901 | expected = %{"TemplateData" => ~s({"data1":"data1","data2":"data2"})} 902 | assert expected == SES.test_render_email_template(template_name, template_data).data 903 | end 904 | end 905 | 906 | test "#create custom email verification template" do 907 | template_name = "MyTemplate" 908 | from_email_address = "test@example.com" 909 | template_subject = "Verified with ExAWS!" 910 | template_content = "This is some custom content" 911 | success_redirection_url = "https://example.com/success" 912 | failure_redirection_url = "https://example.com/failure" 913 | 914 | expected = %{ 915 | "Action" => "CreateCustomVerificationEmailTemplate", 916 | "TemplateName" => template_name, 917 | "FromEmailAddress" => from_email_address, 918 | "TemplateSubject" => template_subject, 919 | "TemplateContent" => template_content, 920 | "SuccessRedirectionURL" => success_redirection_url, 921 | "FailureRedirectionURL" => failure_redirection_url 922 | } 923 | 924 | assert expected == 925 | SES.create_custom_verification_email_template( 926 | template_name, 927 | from_email_address, 928 | template_subject, 929 | template_content, 930 | success_redirection_url, 931 | failure_redirection_url 932 | ).params 933 | end 934 | 935 | test "#update custom email verification template" do 936 | template_name = "MyTemplate" 937 | from_email_address = "test@example.com" 938 | template_subject = "Verified with ExAWS!" 939 | template_content = "This is some custom content" 940 | success_redirection_url = "https://example.com/success" 941 | failure_redirection_url = "https://example.com/failure" 942 | 943 | expected = %{ 944 | "Action" => "UpdateCustomVerificationEmailTemplate", 945 | "TemplateName" => template_name, 946 | "FromEmailAddress" => from_email_address, 947 | "TemplateSubject" => template_subject, 948 | "TemplateContent" => template_content, 949 | "SuccessRedirectionURL" => success_redirection_url, 950 | "FailureRedirectionURL" => failure_redirection_url 951 | } 952 | 953 | assert expected == 954 | SES.update_custom_verification_email_template( 955 | template_name: template_name, 956 | from_email_address: from_email_address, 957 | template_subject: template_subject, 958 | template_content: template_content, 959 | success_redirection_url: success_redirection_url, 960 | failure_redirection_url: failure_redirection_url 961 | ).params 962 | end 963 | 964 | test "#delete custom email verification template" do 965 | template_name = "MyTemplate" 966 | 967 | expected = %{ 968 | "Action" => "DeleteCustomVerificationEmailTemplate", 969 | "TemplateName" => template_name 970 | } 971 | 972 | assert expected == SES.delete_custom_verification_email_template(template_name).params 973 | end 974 | 975 | test "#list custom verification email templates" do 976 | max_results = 25 977 | next_token = "token" 978 | 979 | expected = %{ 980 | "Action" => "ListCustomVerificationEmailTemplates", 981 | "MaxResults" => max_results, 982 | "NextToken" => next_token 983 | } 984 | 985 | assert expected == 986 | SES.list_custom_verification_email_templates(max_results: max_results, next_token: next_token).params 987 | end 988 | 989 | test "#send verification email with custom template" do 990 | template_name = "MyTemplate" 991 | email_address = "test@example.com" 992 | configuration_set_name = "MyConfigurationSet" 993 | 994 | expected = %{ 995 | "Action" => "SendCustomVerificationEmail", 996 | "TemplateName" => template_name, 997 | "EmailAddress" => email_address, 998 | "ConfigurationSetName" => configuration_set_name 999 | } 1000 | 1001 | assert expected == 1002 | SES.send_custom_verification_email(email_address, template_name, 1003 | configuration_set_name: configuration_set_name 1004 | ).params 1005 | end 1006 | 1007 | describe "list_custom_verification_email_templates_v2/1" do 1008 | test "with options" do 1009 | expected = "/v2/email/custom-verification-email-templates?NextToken=QUFBQUF&PageSize=1" 1010 | assert expected == SES.list_custom_verification_email_templates_v2(%{page_size: 1, next_token: "QUFBQUF"}).path 1011 | end 1012 | 1013 | test "without options" do 1014 | expected = "/v2/email/custom-verification-email-templates?" 1015 | assert expected == SES.list_custom_verification_email_templates_v2().path 1016 | end 1017 | end 1018 | 1019 | describe "get_custom_verification_email_templates_v2/1" do 1020 | test "with param" do 1021 | template_name = "MyTemplate" 1022 | expected = "/v2/email/custom-verification-email-templates/#{template_name}" 1023 | assert expected == SES.get_custom_verification_email_template_v2(template_name).path 1024 | end 1025 | end 1026 | 1027 | describe "create_custom_verification_email_template_v2/6" do 1028 | test "with params" do 1029 | template_name = "MyTemplate" 1030 | from_email_address = "test@example.com" 1031 | template_subject = "Verified with ExAWS!" 1032 | template_content = "This is some custom content" 1033 | success_redirection_url = "https://example.com/success" 1034 | failure_redirection_url = "https://example.com/failure" 1035 | 1036 | expected = %{ 1037 | "TemplateName" => template_name, 1038 | "FromEmailAddress" => from_email_address, 1039 | "TemplateSubject" => template_subject, 1040 | "TemplateContent" => template_content, 1041 | "SuccessRedirectionURL" => success_redirection_url, 1042 | "FailureRedirectionURL" => failure_redirection_url 1043 | } 1044 | 1045 | assert expected == 1046 | SES.create_custom_verification_email_template_v2( 1047 | template_name, 1048 | from_email_address, 1049 | template_subject, 1050 | template_content, 1051 | success_redirection_url, 1052 | failure_redirection_url 1053 | ).data 1054 | end 1055 | end 1056 | 1057 | describe "update_custom_verification_email_template_v2/2" do 1058 | test "with all options" do 1059 | template_name = "MyTemplate" 1060 | from_email_address = "test@example.com" 1061 | template_subject = "Verified with ExAWS!" 1062 | template_content = "This is some custom content" 1063 | success_redirection_url = "https://example.com/success" 1064 | failure_redirection_url = "https://example.com/failure" 1065 | 1066 | expected = %{ 1067 | "FromEmailAddress" => from_email_address, 1068 | "TemplateSubject" => template_subject, 1069 | "TemplateContent" => template_content, 1070 | "SuccessRedirectionURL" => success_redirection_url, 1071 | "FailureRedirectionURL" => failure_redirection_url 1072 | } 1073 | 1074 | assert expected == 1075 | SES.update_custom_verification_email_template_v2(template_name, 1076 | from_email_address: from_email_address, 1077 | template_subject: template_subject, 1078 | template_content: template_content, 1079 | success_redirection_url: success_redirection_url, 1080 | failure_redirection_url: failure_redirection_url 1081 | ).data 1082 | end 1083 | 1084 | test "without options" do 1085 | template_name = "MyTemplate" 1086 | assert %{} == SES.update_custom_verification_email_template_v2(template_name).data 1087 | end 1088 | end 1089 | 1090 | describe "delete_custom_verification_email_template_v2/1" do 1091 | test "with param" do 1092 | template_name = "MyTemplate" 1093 | expected = "/v2/email/custom-verification-email-templates/#{template_name}" 1094 | assert expected == SES.delete_custom_verification_email_template_v2(template_name).path 1095 | end 1096 | end 1097 | 1098 | describe "send_custom_verification_email_v2/3" do 1099 | test "with required params" do 1100 | template_name = "MyTemplate" 1101 | email_address = "test@example.com" 1102 | 1103 | expected = %{ 1104 | "TemplateName" => template_name, 1105 | "EmailAddress" => email_address 1106 | } 1107 | 1108 | assert expected == 1109 | SES.send_custom_verification_email_v2(email_address, template_name).data 1110 | end 1111 | 1112 | test "with all options" do 1113 | template_name = "MyTemplate" 1114 | email_address = "test@example.com" 1115 | configuration_set_name = "MyConfigurationSet" 1116 | 1117 | expected = %{ 1118 | "TemplateName" => template_name, 1119 | "EmailAddress" => email_address, 1120 | "ConfigurationSetName" => configuration_set_name 1121 | } 1122 | 1123 | assert expected == 1124 | SES.send_custom_verification_email_v2(email_address, template_name, 1125 | configuration_set_name: configuration_set_name 1126 | ).data 1127 | end 1128 | end 1129 | end 1130 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start(exclude: [:integration]) 2 | --------------------------------------------------------------------------------