├── .formatter.exs ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config └── config.exs ├── lib ├── sendgrid.ex └── sendgrid │ ├── email.ex │ ├── mail.ex │ ├── marketing_campaigns │ └── contacts │ │ ├── lists.ex │ │ ├── recipient.ex │ │ └── recipients.ex │ ├── personalization.ex │ └── response.ex ├── mix.exs ├── mix.lock └── test ├── email_test.exs ├── mail_test.exs ├── support ├── email_view.ex └── templates │ └── email │ ├── layout.html.eex │ ├── layout.txt.eex │ ├── test.html.eex │ ├── test.txt.eex │ └── test2.txt └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["lib/**/*.ex", "test/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | sendgridelixir.iml 3 | /_build 4 | /cover 5 | /deps 6 | erl_crash.dump 7 | *.ez 8 | /doc 9 | .DS_Store -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.0.0 (2019-3-15) 4 | 5 | ### Enhancements 6 | * Switch from using `HTTPoison` to using `Tesla` 7 | * Switch from using `Poison` to `Jason` for JSON serialization 8 | * `SendGrid.Email` is now responsible for configuring its own JSON serialization 9 | * An API key can be set manually for any API call as a keyword option, bypassing 10 | the need for a global API key 11 | * An API key can be set with the `{:system, "ENV_VAR"}` to use values at runtime 12 | * Add dialyzer for validing typespecs 13 | * Add support for dynamic template data 14 | * Add struct for handling Contact Recipient encoding 15 | * Working config added for testing 16 | * Add support for multiple personalizations in a single sent Email request 17 | 18 | ### Bug Fixes 19 | * Typespecs are now correct 20 | 21 | ### Breaking Changes 22 | * `SendGrid.Mailer` has been renamed to `SendGrid.Mail` to better reflect the 23 | actual shape of SendGrid's API structure 24 | * `SendGrid.Contacts.Recipients.add` now requires a recipient to be built with 25 | `SendGrid.Contacts.Recipient.build/2` 26 | * `SendGrid.Contacts.Lists.add` returns `{:ok, map()}` instead of `{:ok, integer()}`. 27 | 28 | ## 1.8.0 (2018-1-18) 29 | 30 | ### Enhancements 31 | * Raise runtime error whenever API isn't configured whenever making an API call 32 | 33 | ### Bug Fixes 34 | * custom headers are properly sent when V3 of the SendGrid API 35 | 36 | ## 1.7.0 (2017-9-11) 37 | 38 | ### Enhancements 39 | * Add `add/1`, `all_recipients/3`, and `delete_recipient/2` to `SendGrid.Contacts.Lists` 40 | * Remove compile warnings for `Phoenix.View` 41 | 42 | ## 1.6.0 (2017-7-14) 43 | 44 | ### Enhancements 45 | * Relax dependency versions 46 | * add `put_phoenix_layout/2` in `SendGrid.Email` to render views in 47 | ### Breaking Changes 48 | * `put_phoenix_template/3` now expects an atom for implicit template rendering 49 | 50 | ## 1.5.0 (2017-7-3) 51 | 52 | ### Enhancements 53 | * update docs 54 | * upgrade to Elixir 1.4 55 | * add support for Phoenix Views 56 | 57 | ## 1.4.0 (2017-2-15) 58 | 59 | ### Enhancements 60 | * update `httpoison` to 0.11.0 and `poison` to 3.0 61 | * clean up compiler warnings when using Elixir 1.3 62 | 63 | ## 1.3.0 (2016-11-5) 64 | 65 | ### Enhancements 66 | * add `add_custom_arg` for custom arguments 67 | * remove `raise` when no API key is provided at compile-time 68 | 69 | ## 1.2.0 (2016-9-28) 70 | 71 | ### Enhancements 72 | * add `add_attachment` for attachments 73 | * bump `:poison` version to 2 74 | 75 | ## 1.1.0 (2016-8-30) 76 | 77 | ### Enhancements 78 | * add `add_header` to be sent with an email 79 | 80 | ## 1.0.3 (2016-8-3) 81 | 82 | ### Bug Fixes 83 | * replace documentation using to `put_to` with `add_to` 84 | 85 | ## 1.0.2 (2016-7-20) 86 | 87 | ### Enhancements 88 | * [Mailer] sandbox mode is fetched during runtime instead of compile time 89 | 90 | ### Bug Fixes 91 | * [Mailer] add missing insertion of template id 92 | 93 | ## 1.0.1 (2016-7-16) 94 | 95 | ### Bug Fixes 96 | * [Email] Make an exposed method private 97 | 98 | ## 1.0.0 (2016-7-15) 99 | 100 | ### Enhancements 101 | * [Email] multiple TO recipients can be added with `add_to/2` and `add_to/3` 102 | * [Email] BCC recipients can be supported 103 | * [Email] Reply-to name can be specified as third param of `put_reply_to/3` 104 | * [Email] added `put_send_at/2` for delayed sending of email 105 | * [Mailer] uses V3 of the SendGrid mail send API 106 | * [Mailer] sandbox mode can be enabled through a config setting 107 | 108 | ### Breaking Changes 109 | * `put_to/2` no longer exists; use `add_to/2` or `add_to/3` instead 110 | * `add_cc/2` when submitting a list of addresses no longer exists 111 | * `put_from_name/2` no longer exists; use `put_from/3` and set the **from_name** as the third param 112 | * `delete_cc/2` no longer exists 113 | 114 | ## 0.1.1 (2016-7-5) 115 | 116 | ### Enhancements 117 | * Updated HTTPoison version for less compiler warnings when using Elixir 1.3 118 | 119 | ## 0.1.0 (2016-5-22) 120 | 121 | ### Enhancements 122 | * Added some API to add email addresses for marketing campaigns 123 | 124 | ### Upgrading From Prior Versions 125 | 126 | `:sendgrid` needs to be added to the list of applications in the `mix.exs` file. 127 | 128 | ```elixir 129 | def application do 130 | [applications: [:sendgrid]] 131 | end 132 | ``` 133 | 134 | ## 0.2.0 (2016-5-22) 135 | 136 | ### Bug Fixes 137 | * Updated some docs 138 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 2 | 3 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SendGrid 2 | 3 | A wrapper for SendGrid's API to create composable emails. 4 | Check the [docs](https://hexdocs.pm/sendgrid/) for complete usage. 5 | 6 | ## Example 7 | 8 | ```elixir 9 | SendGrid.Email.build() 10 | |> SendGrid.Email.add_to("test@email.com") 11 | |> SendGrid.Email.put_from("test2@email.com") 12 | |> SendGrid.Email.put_subject("Hello from Elixir") 13 | |> SendGrid.Email.put_text("Sent with Elixir") 14 | |> SendGrid.Mail.send() 15 | ``` 16 | 17 | ## Installation 18 | 19 | Add the following code to your dependencies in your **`mix.exs`** file: 20 | 21 | ```elixir 22 | {:sendgrid, "~> 2.0"} 23 | ``` 24 | 25 | ## Configuration 26 | 27 | In one of your configuration files, include your SendGrid API key like this: 28 | 29 | ```elixir 30 | config :sendgrid, 31 | api_key: "SENDGRID_API_KEY" 32 | ``` 33 | 34 | If you want to use environment variable, use `{:system, "ENV_NAME"}` in your config: 35 | 36 | ```elixir 37 | config :sendgrid, 38 | api_key: {:system, "SENDGRID_API_KEY"} 39 | ``` 40 | 41 | If you'd like to enable sandbox mode (emails won't send but will be validated), add the setting to your config: 42 | 43 | ```elixir 44 | config :sendgrid, 45 | api_key: "SENDGRID_API_KEY", 46 | sandbox_enable: true 47 | ``` 48 | 49 | Add `:sendgrid` to your list of applications if using Elixir 1.3 or lower. 50 | 51 | ```elixir 52 | defp application do 53 | [applications: [:sendgrid]] 54 | end 55 | ``` 56 | 57 | ## Phoenix Views 58 | 59 | You can use Phoenix Views to set your HTML and text content of your emails. You just have 60 | to provide a view module and template name and you're good to go! Additionally, you can set 61 | a layout to render the view in with `SendGrid.Email.put_phoenix_layout/2`. See `SendGrid.Email.put_phoenix_template/3` 62 | for complete usage. 63 | 64 | ### Examples 65 | 66 | ```elixir 67 | import SendGrid.Email 68 | 69 | # Using an HTML template 70 | %SendGrid.Email{} 71 | |> put_phoenix_view(MyApp.Web.EmailView) 72 | |> put_phoenix_template("welcome_email.html", user: user) 73 | 74 | # Using a text template 75 | %SendGrid.Email{} 76 | |> put_phoenix_view(MyApp.Web.EmailView) 77 | |> put_phoenix_template("welcome_email.txt", user: user) 78 | 79 | # Using both an HTML and text template 80 | %SendGrid.Email{} 81 | |> put_phoenix_view(MyApp.Web.EmailView) 82 | |> put_phoenix_template(:welcome_email, user: user) 83 | 84 | 85 | # Setting the layout 86 | %SendGrid.Email{} 87 | |> put_phoenix_layout({MyApp.Web.EmailView, :layout}) 88 | |> put_phoenix_view(MyApp.Web.EmailView) 89 | |> put_phoenix_template(:welcome_email, user: user) 90 | ``` 91 | 92 | ### Using a Default Phoenix View 93 | 94 | You can set a default Phoenix View to use for rendering templates. Just set the `:phoenix_view` config value 95 | 96 | ```elixir 97 | config :sendgrid, 98 | phoenix_view: MyApp.Web.EmailView 99 | ``` 100 | 101 | ### Using a Default Layout 102 | 103 | You can set a default layout to render your view in. Set the `:phoenix_layout` config value. 104 | 105 | ```elixir 106 | config :sendgrid, 107 | phoenix_layout: {MyApp.Web.EmailView, :layout} 108 | ``` 109 | 110 | ## Personalizations 111 | 112 | Personalizations are used to identify who should receive the email as well as specifics about how you would like the email to be handled. 113 | 114 | Personalizations allow you to define: 115 | 116 | - `to`, `cc`, `bcc` - The recipients of your email. 117 | - `subject` - The subject of your email. 118 | - `headers` - Any headers you would like to include in your email. 119 | - `substitutions` - Any substitutions you would like to be made for your email. 120 | - `custom_args` - Any custom arguments you would like to include in your email. 121 | - `dynamic_template_data` - Data to send along with a template. 122 | - `send_at` - A specific time that you would like your email to be sent. 123 | 124 | An `SendGrid.Email` automatically takes these fields and transforms them into a personalization to be sent in the email. However, you can add multiple personalizations to an email and specify different handling instructions for different copies of your email. For example, you could send the same email to both and , but set each email to be delivered at different times. 125 | 126 | ### Example 127 | 128 | ```elixir 129 | alias SendGrid.{Mail, Email} 130 | personalization_1 = 131 | Email.build() 132 | |> Email.add_to("john@example.com") 133 | |> Email.put_subject("Exciting news!") 134 | |> Email.to_personalization() 135 | 136 | personalization_2 = 137 | Email.build() 138 | |> Email.add_to("jane@example.com") 139 | |> Email.put_subject("We've some exciting news!") 140 | |> Email.to_personalization() 141 | 142 | Email.build() 143 | |> Email.put_from("news@mydomain.com") 144 | |> Email.put_text("...") 145 | |> Email.put_html("...") 146 | |> Email.add_personalization(personalization_1) 147 | |> Email.add_personalization(personalization_2) 148 | |> Mail.send() 149 | ``` 150 | 151 | ### Limitations 152 | 153 | The SendGrid v3 API limits you to 1,000 personalizations per API request. If you need to include more than 1,000 personalizations, please divide these across multiple API requests. 154 | 155 | ## Testing 156 | 157 | To run the unit tests you will need to create a `config/config.exs` file and provide your own SendGrid API and email address to receive a test email. 158 | 159 | ```elixir 160 | use Mix.Config 161 | 162 | config :sendgrid, 163 | api_key: "", 164 | phoenix_view: SendGrid.Email.Test.EmailView, 165 | test_address: "recipient@example.com" 166 | ``` 167 | 168 | The `config` directory is excluded from the git repository so your API key and email address will not be committed. 169 | 170 | Once configured you can run the full test suite including integration tests as follows: 171 | 172 | ```console 173 | mix test --include integration 174 | ``` 175 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :sendgrid, 4 | api_key: {:system, "SENDGRID_API_KEY"}, 5 | sandbox_enable: true, 6 | phoenix_view: SendGrid.EmailView, 7 | test_address: System.get_env("SENDGRID_TEST_EMAIL") 8 | -------------------------------------------------------------------------------- /lib/sendgrid.ex: -------------------------------------------------------------------------------- 1 | defmodule SendGrid do 2 | @moduledoc """ 3 | Interface to SendGrid's API. 4 | 5 | ## Configuration 6 | 7 | An API key can be set in your application's config. 8 | 9 | # Compile-time configured key. 10 | config :sendgrid, 11 | api_key: "sendgrid_api_key" 12 | 13 | # Run-time configured key 14 | config :sendgrid, 15 | api_key: {:system, "ENV_KEY"} 16 | 17 | Optionally you can supply an API key as a keyword option in the last argument 18 | of any API call to override and set the API key to use for the request. 19 | 20 | SendGrid.Mail.send(..., api_key: "API_KEY") 21 | 22 | ## Usage 23 | 24 | Most usage with this library will be with composing transactional emails. 25 | Refer to `SendGrid.Email` for full documentation and usage. 26 | """ 27 | 28 | alias SendGrid.Response 29 | 30 | @type api_key :: {:api_key, String.t()} 31 | @type query :: {:query, Keyword.t()} 32 | @type page :: {:page, pos_integer()} 33 | @type page_size :: {:page_size, pos_integer()} 34 | 35 | @typedoc """ 36 | Optional arguments to use when performing a request. 37 | """ 38 | @type options :: [query | api_key] 39 | 40 | @doc """ 41 | Performs a GET request. 42 | 43 | ## Options 44 | 45 | * `:api_key` - API key to use with the request. 46 | * `:query` - Keyword list of query params to use with the request. 47 | """ 48 | @spec get(path :: String.t(), options :: options()) :: {:ok, Response.t()} | {:error, any()} 49 | def get(path, opts \\ []) when is_list(opts) do 50 | opts 51 | |> api_key() 52 | |> build_client() 53 | |> Tesla.get(path, query_opts(opts)) 54 | |> parse_response() 55 | end 56 | 57 | @doc """ 58 | Performs a POST request. 59 | 60 | ## Options 61 | 62 | * `:api_key` - API key to use with the request. 63 | * `:query` - Keyword list of query params to use with the request. 64 | """ 65 | @spec post(path :: String.t(), body :: map(), options :: options()) :: 66 | {:ok, Response.t()} | {:error, any()} 67 | def post(path, body, opts \\ []) when is_map(body) and is_list(opts) do 68 | opts 69 | |> api_key() 70 | |> build_client() 71 | |> Tesla.post(path, body, query_opts(opts)) 72 | |> parse_response() 73 | end 74 | 75 | @doc """ 76 | Performs a PATCH request. 77 | 78 | ## Options 79 | 80 | * `:api_key` - API key to use with the request. 81 | * `:query` - Keyword list of query params to use with the request. 82 | """ 83 | @spec patch(path :: String.t(), body :: map(), options :: options()) :: 84 | {:ok, Response.t()} | {:error, any()} 85 | def patch(path, body, opts \\ []) when is_map(body) and is_list(opts) do 86 | opts 87 | |> api_key() 88 | |> build_client() 89 | |> Tesla.patch(path, body, query_opts(opts)) 90 | |> parse_response() 91 | end 92 | 93 | @doc """ 94 | Performs a DELETE request. 95 | 96 | ## Options 97 | 98 | * `:api_key` - API key to use with the request. 99 | * `:query` - Keyword list of query params to use with the request. 100 | """ 101 | @spec delete(path :: String.t(), options :: options()) :: {:ok, Response.t()} | {:error, any()} 102 | def delete(path, opts \\ []) when is_list(opts) do 103 | opts 104 | |> api_key() 105 | |> build_client() 106 | |> Tesla.delete(path, query_opts(opts)) 107 | |> parse_response() 108 | end 109 | 110 | defp api_key(opts) do 111 | api_key = Keyword.get(opts, :api_key) || runtime_key() 112 | 113 | unless api_key do 114 | raise RuntimeError, """ 115 | No API key is configured for SendGrid. Update your config your pass in a 116 | key with `:api_key` as an addional request option. 117 | 118 | SendGrid.get("/stats", api_key: "API_KEY") 119 | 120 | config :sendgrid, 121 | api_key: "sendgrid_api_key" 122 | 123 | config :sendgrid, 124 | api_key: {:system, "SENDGRID_KEY"} 125 | """ 126 | end 127 | 128 | api_key 129 | end 130 | 131 | defp runtime_key do 132 | case Application.get_env(:sendgrid, :api_key) do 133 | {:system, env_key} -> System.get_env(env_key) 134 | key -> key 135 | end 136 | end 137 | 138 | defp build_client(api_key) do 139 | middleware = [ 140 | {Tesla.Middleware.BaseUrl, "https://api.sendgrid.com"}, 141 | Tesla.Middleware.JSON, 142 | {Tesla.Middleware.Headers, [{"Authorization", "Bearer #{api_key}"}]} 143 | ] 144 | 145 | Tesla.client(middleware) 146 | end 147 | 148 | defp query_opts(opts) do 149 | Keyword.take(opts, [:query]) 150 | end 151 | 152 | defp parse_response({:ok, %{body: body, headers: headers, status: status}}) do 153 | {:ok, %Response{body: body, headers: headers, status: status}} 154 | end 155 | 156 | defp parse_response({:error, _} = error), do: error 157 | end 158 | -------------------------------------------------------------------------------- /lib/sendgrid/email.ex: -------------------------------------------------------------------------------- 1 | defmodule SendGrid.Email do 2 | @moduledoc """ 3 | Email primitive for composing emails with SendGrid's API. 4 | 5 | You can easily compose on an Email to set the fields of your email. 6 | 7 | ## Example 8 | 9 | Email.build() 10 | |> Email.add_to("test@email.com") 11 | |> Email.put_from("test2@email.com") 12 | |> Email.put_subject("Hello from Elixir") 13 | |> Email.put_text("Sent with Elixir") 14 | |> SendGrid.Mail.send() 15 | 16 | ## SendGrid Specific Features 17 | 18 | Many common features of SendGrid V3 API for transactional emails are supported. 19 | 20 | ### Templates 21 | 22 | You can use a SendGrid template by providing a template id. 23 | 24 | put_template(email, "some_template_id") 25 | 26 | ### Substitutions 27 | 28 | You can provided a key-value pair for subsititions to have text replaced. 29 | 30 | add_substitution(email, "-key-", "value") 31 | 32 | ### Scheduled Sending 33 | 34 | You can provide a Unix timestamp to have an email delivered in the future. 35 | 36 | send_at(email, 1409348513) 37 | 38 | ## Phoenix Views 39 | 40 | You can use Phoenix Views to set your HTML and text content of your emails. You just have 41 | to provide a view module and template name and you're good to go! Additionally, you can set 42 | a layout to render the view in with `put_phoenix_layout/2`. See `put_phoenix_template/3` 43 | for complete usage. 44 | 45 | ### Examples 46 | 47 | # Using an HTML template 48 | %Email{} 49 | |> put_phoenix_view(MyApp.Web.EmailView) 50 | |> put_phoenix_template("welcome_email.html", user: user) 51 | 52 | # Using a text template 53 | %Email{} 54 | |> put_phoenix_view(MyApp.Web.EmailView) 55 | |> put_phoenix_template("welcome_email.txt", user: user) 56 | 57 | # Using both an HTML and text template 58 | %Email{} 59 | |> put_phoenix_view(MyApp.Web.EmailView) 60 | |> put_phoenix_template(:welcome_email, user: user) 61 | 62 | # Setting the layout 63 | %Email{} 64 | |> put_phoenix_layout({MyApp.Web.EmailView, :layout}) 65 | |> put_phoenix_view(MyApp.Web.EmailView) 66 | |> put_phoenix_template(:welcome_email, user: user) 67 | 68 | 69 | ### Using a Default Phoenix View 70 | 71 | You can set a default Phoenix View to use for rendering templates. Just set the `:phoenix_view` 72 | config value. 73 | 74 | config :sendgrid, 75 | phoenix_view: MyApp.Web.EmailView 76 | 77 | 78 | ### Using a Default View Layout 79 | 80 | You can set a default layout to render the view in. Just set the `:phoenix_layout` config value. 81 | 82 | config :sendgrid, 83 | phoenix_layout: {MyApp.Web.EmailView, :layout} 84 | 85 | """ 86 | 87 | alias SendGrid.{Email, Personalization} 88 | 89 | defstruct to: nil, 90 | cc: nil, 91 | bcc: nil, 92 | from: nil, 93 | reply_to: nil, 94 | subject: nil, 95 | content: nil, 96 | template_id: nil, 97 | substitutions: nil, 98 | custom_args: nil, 99 | personalizations: nil, 100 | send_at: nil, 101 | headers: nil, 102 | attachments: nil, 103 | dynamic_template_data: nil, 104 | sandbox: false, 105 | __phoenix_view__: nil, 106 | __phoenix_layout__: nil 107 | 108 | @type t :: %Email{ 109 | to: nil | [recipient], 110 | cc: nil | [recipient], 111 | bcc: nil | [recipient], 112 | from: nil | recipient, 113 | reply_to: nil | recipient, 114 | subject: nil | String.t(), 115 | content: nil | [content], 116 | template_id: nil | String.t(), 117 | substitutions: nil | substitutions, 118 | custom_args: nil | custom_args, 119 | personalizations: nil | [Personalization.t()], 120 | dynamic_template_data: nil | dynamic_template_data, 121 | send_at: nil | integer, 122 | headers: nil | headers(), 123 | attachments: nil | [attachment], 124 | sandbox: boolean(), 125 | __phoenix_view__: nil | atom, 126 | __phoenix_layout__: 127 | nil | %{optional(:text) => String.t(), optional(:html) => String.t()} 128 | } 129 | 130 | @type recipient :: %{required(:email) => String.t(), optional(:name) => String.t()} 131 | @type content :: %{type: String.t(), value: String.t()} 132 | @type headers :: %{String.t() => String.t()} 133 | @type attachment :: %{ 134 | required(:content) => String.t(), 135 | optional(:type) => String.t(), 136 | required(:filename) => String.t(), 137 | optional(:disposition) => String.t(), 138 | optional(:content_id) => String.t() 139 | } 140 | 141 | @type substitutions :: %{String.t() => String.t()} 142 | @type custom_args :: %{String.t() => String.t()} 143 | @type dynamic_template_data :: %{String.t() => String.t()} 144 | 145 | @doc """ 146 | Builds an an empty email to compose on. 147 | 148 | ## Examples 149 | 150 | iex> build() 151 | %Email{...} 152 | 153 | """ 154 | @spec build :: t 155 | def build do 156 | %Email{} 157 | end 158 | 159 | @doc """ 160 | Sets the `to` field for the email. A to-name can be passed as the third parameter. 161 | 162 | ## Examples 163 | 164 | add_to(%Email{}, "test@email.com") 165 | add_to(%Email{}, "test@email.com", "John Doe") 166 | 167 | """ 168 | @spec add_to(t, String.t()) :: t 169 | def add_to(%Email{to: to} = email, to_address) do 170 | addresses = add_address_to_list(to, to_address) 171 | %Email{email | to: addresses} 172 | end 173 | 174 | @spec add_to(t, String.t(), String.t()) :: t 175 | def add_to(%Email{to: to} = email, to_address, to_name) do 176 | addresses = add_address_to_list(to, to_address, to_name) 177 | %Email{email | to: addresses} 178 | end 179 | 180 | @doc """ 181 | Sets the `from` field for the email. The from-name can be specified as the third parameter. 182 | 183 | ## Examples 184 | 185 | put_from(%Email{}, "test@email.com") 186 | put_from(%Email{}, "test@email.com", "John Doe") 187 | 188 | """ 189 | @spec put_from(t, String.t()) :: t 190 | def put_from(%Email{} = email, from_address) do 191 | %Email{email | from: address(from_address)} 192 | end 193 | 194 | @spec put_from(t, String.t(), String.t()) :: t 195 | def put_from(%Email{} = email, from_address, from_name) do 196 | %Email{email | from: address(from_address, from_name)} 197 | end 198 | 199 | @doc """ 200 | Add recipients to the `CC` address field. The cc-name can be specified as the third parameter. 201 | 202 | ## Examples 203 | 204 | add_cc(%Email{}, "test@email.com") 205 | add_cc(%Email{}, "test@email.com", "John Doe") 206 | 207 | """ 208 | @spec add_cc(t, String.t()) :: t 209 | def add_cc(%Email{cc: cc} = email, cc_address) do 210 | addresses = add_address_to_list(cc, cc_address) 211 | %Email{email | cc: addresses} 212 | end 213 | 214 | @spec add_cc(Email.t(), String.t(), String.t()) :: Email.t() 215 | def add_cc(%Email{cc: cc} = email, cc_address, cc_name) do 216 | addresses = add_address_to_list(cc, cc_address, cc_name) 217 | %Email{email | cc: addresses} 218 | end 219 | 220 | @doc """ 221 | Add recipients to the `BCC` address field. The bcc-name can be specified as the third parameter. 222 | 223 | ## Examples 224 | 225 | add_bcc(%Email{}, "test@email.com") 226 | add_bcc(%Email{}, "test@email.com", "John Doe") 227 | 228 | """ 229 | @spec add_bcc(t, String.t()) :: t 230 | def add_bcc(%Email{bcc: bcc} = email, bcc_address) do 231 | addresses = add_address_to_list(bcc, bcc_address) 232 | %Email{email | bcc: addresses} 233 | end 234 | 235 | @spec add_bcc(t, String.t(), String.t()) :: t 236 | def add_bcc(%Email{bcc: bcc} = email, bcc_address, bcc_name) do 237 | addresses = add_address_to_list(bcc, bcc_address, bcc_name) 238 | %Email{email | bcc: addresses} 239 | end 240 | 241 | @doc """ 242 | Adds an attachment to the email. 243 | 244 | An attachment is a map with the keys: 245 | 246 | * `:content` 247 | * `:type` 248 | * `:filename` 249 | * `:disposition` 250 | * `:content_id` 251 | 252 | ## Examples 253 | 254 | attachment = %{content: "base64string", filename: "image.jpg"} 255 | add_attachment(%Email{}, attachment} 256 | 257 | """ 258 | @spec add_attachment(t, attachment) :: t 259 | def add_attachment(%Email{} = email, attachment) do 260 | attachments = 261 | case email.attachments do 262 | nil -> [attachment] 263 | list -> list ++ [attachment] 264 | end 265 | 266 | %Email{email | attachments: attachments} 267 | end 268 | 269 | @doc """ 270 | Sets the `reply_to` field for the email. The reply-to name can be specified as the third parameter. 271 | 272 | ## Examples 273 | 274 | put_reply_to(%Email{}, "test@email.com") 275 | put_reply_to(%Email{}, "test@email.com", "John Doe") 276 | 277 | """ 278 | @spec put_reply_to(t, String.t()) :: t 279 | def put_reply_to(%Email{} = email, reply_to_address) do 280 | %Email{email | reply_to: address(reply_to_address)} 281 | end 282 | 283 | @spec put_reply_to(t, String.t(), String.t()) :: t 284 | def put_reply_to(%Email{} = email, reply_to_address, reply_to_name) do 285 | %Email{email | reply_to: address(reply_to_address, reply_to_name)} 286 | end 287 | 288 | @doc """ 289 | Sets the `subject` field for the email. 290 | 291 | ## Examples 292 | 293 | put_subject(%Email{}, "Hello from Elixir") 294 | 295 | """ 296 | @spec put_subject(t, String.t()) :: t 297 | def put_subject(%Email{} = email, subject) do 298 | %Email{email | subject: subject} 299 | end 300 | 301 | @doc """ 302 | Sets `text` content of the email. 303 | 304 | ## Examples 305 | 306 | put_text(%Email{}, "Sent from Elixir!") 307 | 308 | """ 309 | @spec put_text(t, String.t()) :: t 310 | def put_text(%Email{content: [%{type: "text/plain"} | tail]} = email, text_body) do 311 | content = [%{type: "text/plain", value: text_body} | tail] 312 | %Email{email | content: content} 313 | end 314 | 315 | def put_text(%Email{content: content} = email, text_body) do 316 | content = [%{type: "text/plain", value: text_body} | List.wrap(content)] 317 | %Email{email | content: content} 318 | end 319 | 320 | @doc """ 321 | Sets the `html` content of the email. 322 | 323 | ## Examples 324 | 325 | Email.put_html(%Email{}, "

Sent from Elixir!

") 326 | 327 | """ 328 | @spec put_html(t, String.t()) :: t 329 | def put_html(%Email{content: [head | %{type: "text/html"}]} = email, html_body) do 330 | content = [head | %{type: "text/html", value: html_body}] 331 | %Email{email | content: content} 332 | end 333 | 334 | def put_html(%Email{content: content} = email, html_body) do 335 | content = List.wrap(content) ++ [%{type: "text/html", value: html_body}] 336 | %Email{email | content: content} 337 | end 338 | 339 | @doc """ 340 | Sets a custom header. 341 | 342 | ## Examples 343 | 344 | Email.add_header(%Email{}, "HEADER_KEY", "HEADER_VALUE") 345 | 346 | """ 347 | @spec add_header(t, String.t(), String.t()) :: t 348 | def add_header(%Email{headers: headers} = email, header_key, header_value) 349 | when is_binary(header_key) and is_binary(header_value) do 350 | new_headers = Map.put(headers || %{}, header_key, header_value) 351 | %Email{email | headers: new_headers} 352 | end 353 | 354 | @doc """ 355 | Uses a predefined SendGrid template for the email. 356 | 357 | ## Examples 358 | 359 | Email.put_template(%Email{}, "the_template_id") 360 | 361 | """ 362 | @spec put_template(t, String.t()) :: t 363 | def put_template(%Email{} = email, template_id) do 364 | %Email{email | template_id: template_id} 365 | end 366 | 367 | @doc """ 368 | Adds a substitution value to be used with a template. 369 | 370 | If a substitution for a given name is already set, it will be replaced when adding 371 | a substitution with the same name. 372 | 373 | ## Examples 374 | 375 | Email.add_substitution(%Email{}, "-sentIn-", "Elixir") 376 | 377 | """ 378 | @spec add_substitution(t, String.t(), String.t()) :: t 379 | def add_substitution(%Email{substitutions: substitutions} = email, sub_name, sub_value) do 380 | substitutions = Map.put(substitutions || %{}, sub_name, sub_value) 381 | %Email{email | substitutions: substitutions} 382 | end 383 | 384 | @doc """ 385 | Adds a custom_arg value to the email. 386 | 387 | If an argument for a given name is already set, it will be replaced when adding 388 | a argument with the same name. 389 | 390 | ## Examples 391 | 392 | Email.add_custom_arg(%Email{}, "-sentIn-", "Elixir") 393 | 394 | """ 395 | @spec add_custom_arg(t, String.t(), String.t()) :: t 396 | def add_custom_arg(%Email{custom_args: custom_args} = email, arg_name, arg_value) do 397 | custom_args = Map.put(custom_args || %{}, arg_name, arg_value) 398 | %Email{email | custom_args: custom_args} 399 | end 400 | 401 | @doc """ 402 | Adds a custom_arg value to the email. 403 | 404 | If an argument for a given name is already set, it will be replaced when adding 405 | a argument with the same name. 406 | 407 | ## Examples 408 | 409 | Email.add_dynamic_template_data(%Email{}, "-sentIn-", "Elixir") 410 | 411 | """ 412 | @spec add_dynamic_template_data(t, String.t(), String.t()) :: t 413 | def add_dynamic_template_data( 414 | %Email{dynamic_template_data: dynamic_template_data} = email, 415 | arg_name, 416 | arg_value 417 | ) do 418 | dynamic_template_data = Map.put(dynamic_template_data || %{}, arg_name, arg_value) 419 | %Email{email | dynamic_template_data: dynamic_template_data} 420 | end 421 | 422 | @doc """ 423 | Sets a future date of when to send the email. 424 | 425 | ## Examples 426 | 427 | Email.put_send_at(%Email{}, 1409348513) 428 | 429 | """ 430 | @spec put_send_at(t, integer) :: t 431 | def put_send_at(%Email{} = email, send_at) do 432 | %Email{email | send_at: send_at} 433 | end 434 | 435 | defp address(email), do: %{email: email} 436 | defp address(email, name), do: %{email: email, name: name} 437 | 438 | defp add_address_to_list(nil, email) do 439 | [address(email)] 440 | end 441 | 442 | defp add_address_to_list(list, email) when is_list(list) do 443 | list ++ [address(email)] 444 | end 445 | 446 | defp add_address_to_list(nil, email, name) do 447 | [address(email, name)] 448 | end 449 | 450 | defp add_address_to_list(list, email, name) when is_list(list) do 451 | list ++ [address(email, name)] 452 | end 453 | 454 | @doc """ 455 | Sets the layout to use for the Phoenix Template. 456 | 457 | Expects a tuple of the view module and layout to use. If you provide an atom as the second element, 458 | the text and HMTL versions of that template will be used for the respective content types. 459 | 460 | Alernatively, you can set a default layout to use by setting the `:phoenix_view` key in your config as 461 | an atom which will be used for both text and HTML emails. 462 | 463 | config :sendgrid, 464 | phoenix_layout: {MyApp.Web.EmailView, :layout} 465 | 466 | ## Examples 467 | 468 | put_phoenix_layout(email, {MyApp.Web.EmailView, "layout.html"}) 469 | put_phoenix_layout(email, {MyApp.Web.EmailView, "layout.txt"}) 470 | put_phoenix_layout(email, {MyApp.Web.EmailView, :layout}) 471 | 472 | """ 473 | @spec put_phoenix_layout(t, {atom, atom}) :: t 474 | def put_phoenix_layout(%Email{} = email, {module, layout}) 475 | when is_atom(module) and is_atom(layout) do 476 | layouts = build_layouts({module, layout}) 477 | %Email{email | __phoenix_layout__: layouts} 478 | end 479 | 480 | @spec put_phoenix_layout(t, {atom, String.t()}) :: t 481 | def put_phoenix_layout(%Email{__phoenix_layout__: layouts} = email, {module, layout}) 482 | when is_atom(module) do 483 | layouts = layouts || %{} 484 | updated_layout = build_layouts({module, layout}) 485 | %Email{email | __phoenix_layout__: Map.merge(layouts, updated_layout)} 486 | end 487 | 488 | # Build layout map 489 | defp build_layouts({module, layout}) when is_atom(module) and is_atom(layout) do 490 | base_name = Atom.to_string(layout) 491 | 492 | %{ 493 | text: {module, base_name <> ".txt"}, 494 | html: {module, base_name <> ".html"} 495 | } 496 | end 497 | 498 | defp build_layouts({module, layout} = args) when is_atom(module) do 499 | case Path.extname(layout) do 500 | ".html" -> %{html: args} 501 | ".txt" -> %{text: args} 502 | _ -> raise ArgumentError, "unsupported file type" 503 | end 504 | end 505 | 506 | @doc """ 507 | Sets the Phoenix View to use. 508 | 509 | This will override the default Phoenix View if set in under the `:phoenix_view` 510 | config value. 511 | 512 | ## Examples 513 | 514 | put_phoenix_view(email, MyApp.Web.EmailView) 515 | 516 | """ 517 | @spec put_phoenix_view(t, atom) :: t 518 | def put_phoenix_view(%Email{} = email, module) when is_atom(module) do 519 | %Email{email | __phoenix_view__: module} 520 | end 521 | 522 | @doc """ 523 | Renders the Phoenix template with the given assigns. 524 | 525 | You can set the default Phoenix View to use for your templates by setting the `:phoenix_view` config value. 526 | Additionally, you can set the view on a per email basis by calling `put_phoenix_view/2`. Furthermore, you can have 527 | the template rendered inside a layout. See `put_phoenix_layout/2` for more details. 528 | 529 | ## Explicit Template Extensions 530 | 531 | You can provide a template name with an explicit extension such as `"some_template.html"` or 532 | `"some_template.txt"`. This is set the content of the email respective to the content type of 533 | the template rendered. For example, if you render an HTML template, the output of the rendering 534 | will be the HTML content of the email. 535 | 536 | ## Implicit Template Extensions 537 | 538 | You can omit a template's extension and attempt to have both a text template and HTML template 539 | rendered. To have both types rendered, both templates must share the same base file name. For 540 | example, if you have a template named `"some_template.txt"` and a template named `"some_template.html"` 541 | and you call `put_phoenix_template(email, :some_template)`, both templates will be used and will 542 | set the email content for both content types. The only caveat is *both files must exist*, otherwise you'll 543 | have an exception raised. 544 | 545 | ## Examples 546 | 547 | iex> put_phoenix_template(email, "some_template.html") 548 | %Email{content: [%{type: "text/html", value: ...}], ...} 549 | 550 | iex> put_phoenix_template(email, "some_template.txt", name: "John Doe") 551 | %Email{content: [%{type: "text/plain", value: ...}], ...} 552 | 553 | iex> put_phoenix_template(email, :some_template, user: user) 554 | %Email{content: [%{type: "text/plain", value: ...}, %{type: "text/html", value: ...}], ...} 555 | 556 | """ 557 | def put_phoenix_template(email, template_name, assigns \\ []) 558 | @spec put_phoenix_template(t, atom, list()) :: t 559 | def put_phoenix_template(%Email{} = email, template_name, assigns) 560 | when is_atom(template_name) do 561 | with true <- ensure_phoenix_loaded(), 562 | view_mod <- phoenix_view_module(email), 563 | layouts <- phoenix_layouts(email), 564 | template_name <- Atom.to_string(template_name) do 565 | email 566 | |> render_html(view_mod, template_name <> ".html", layouts, assigns) 567 | |> render_text(view_mod, template_name <> ".txt", layouts, assigns) 568 | end 569 | end 570 | 571 | @spec put_phoenix_template(t, String.t(), list()) :: t 572 | def put_phoenix_template(%Email{} = email, template_name, assigns) do 573 | with true <- ensure_phoenix_loaded(), 574 | view_mod <- phoenix_view_module(email), 575 | layouts <- phoenix_layouts(email) do 576 | case Path.extname(template_name) do 577 | ".html" -> 578 | render_html(email, view_mod, template_name, layouts, assigns) 579 | 580 | ".txt" -> 581 | render_text(email, view_mod, template_name, layouts, assigns) 582 | end 583 | end 584 | end 585 | 586 | defp render_html(email, view_mod, template_name, layouts, assigns) do 587 | assigns = 588 | if Map.has_key?(layouts, :html) do 589 | Keyword.put(assigns, :layout, Map.get(layouts, :html)) 590 | else 591 | assigns 592 | end 593 | 594 | html = Phoenix.View.render_to_string(view_mod, template_name, assigns) 595 | put_html(email, html) 596 | end 597 | 598 | defp render_text(email, view_mod, template_name, layouts, assigns) do 599 | assigns = 600 | if Map.has_key?(layouts, :text) do 601 | Keyword.put(assigns, :layout, Map.get(layouts, :text)) 602 | else 603 | assigns 604 | end 605 | 606 | text = Phoenix.View.render_to_string(view_mod, template_name, assigns) 607 | put_text(email, text) 608 | end 609 | 610 | defp ensure_phoenix_loaded do 611 | unless Code.ensure_loaded?(Phoenix) do 612 | raise ArgumentError, 613 | "Attempted to call function that depends on Phoenix. " <> 614 | "Make sure Phoenix is part of your dependencies" 615 | end 616 | 617 | true 618 | end 619 | 620 | defp phoenix_layouts(%Email{__phoenix_layout__: layouts}) do 621 | layouts = layouts || %{} 622 | 623 | case config(:phoenix_layout) do 624 | nil -> 625 | layouts 626 | 627 | {module, layout} when is_atom(module) and is_atom(layout) -> 628 | configured_layouts = build_layouts({module, layout}) 629 | Map.merge(configured_layouts, layouts) 630 | 631 | _ -> 632 | raise ArgumentError, 633 | "Invalid configuration set for :phoenix_layout. " <> 634 | "Ensure the configuration is a tuple of a module and atom ({MyApp.View, :layout})." 635 | end 636 | end 637 | 638 | defp phoenix_view_module(%Email{__phoenix_view__: nil}) do 639 | mod = config(:phoenix_view) 640 | 641 | unless mod do 642 | raise ArgumentError, 643 | "Phoenix view is expected to be set or configured. " <> 644 | "Ensure your config for :sendgrid includes a value for :phoenix_view or " <> 645 | "explicity set the Phoenix view with `put_phoenix_view/2`." 646 | end 647 | 648 | mod 649 | end 650 | 651 | defp phoenix_view_module(%Email{__phoenix_view__: view_module}), do: view_module 652 | 653 | @doc """ 654 | Sets the email to be sent with sandbox mode enabled or disabled. 655 | 656 | The sandbox mode will default to what is explicity configured with 657 | SendGrid's configuration. 658 | """ 659 | @spec set_sandbox(t(), boolean()) :: t() 660 | def set_sandbox(%Email{} = email, enabled?) when is_boolean(enabled?) do 661 | %Email{email | sandbox: enabled?} 662 | end 663 | 664 | @doc """ 665 | Transforms an `t:Email.t/0` to a `t:Personalization.t/0`. 666 | """ 667 | @spec to_personalization(t()) :: Personalization.t() 668 | def to_personalization(%Email{} = email) do 669 | %Personalization{ 670 | to: email.to, 671 | cc: email.cc, 672 | bcc: email.bcc, 673 | subject: email.subject, 674 | substitutions: email.substitutions, 675 | custom_args: email.custom_args, 676 | dynamic_template_data: email.dynamic_template_data, 677 | send_at: email.send_at, 678 | headers: email.headers 679 | } 680 | end 681 | 682 | @doc """ 683 | Adds a `t:Personalization.t/0` to an email. 684 | """ 685 | @spec add_personalization(t(), Personalization.t()) :: t() 686 | def add_personalization(%Email{} = email, %Personalization{} = personalization) do 687 | personalizations = List.wrap(email.personalizations) ++ [personalization] 688 | 689 | %Email{email | personalizations: personalizations} 690 | end 691 | 692 | defp config(key) do 693 | Application.get_env(:sendgrid, key) 694 | end 695 | 696 | defimpl Jason.Encoder do 697 | def encode(%Email{personalizations: [_ | _]} = email, opts) do 698 | params = %{ 699 | personalizations: email.personalizations, 700 | from: email.from, 701 | subject: email.subject, 702 | content: email.content, 703 | reply_to: email.reply_to, 704 | send_at: email.send_at, 705 | template_id: email.template_id, 706 | attachments: email.attachments, 707 | headers: email.headers, 708 | mail_settings: %{ 709 | sandbox_mode: %{ 710 | enable: Application.get_env(:sendgrid, :sandbox_enable, email.sandbox) 711 | } 712 | } 713 | } 714 | 715 | Jason.Encode.map(params, opts) 716 | end 717 | 718 | def encode(%Email{personalizations: nil} = email, opts) do 719 | personalization = Email.to_personalization(email) 720 | 721 | email 722 | |> Email.add_personalization(personalization) 723 | |> encode(opts) 724 | end 725 | end 726 | end 727 | -------------------------------------------------------------------------------- /lib/sendgrid/mail.ex: -------------------------------------------------------------------------------- 1 | defmodule SendGrid.Mail do 2 | @moduledoc """ 3 | Module for sending transactional email. 4 | 5 | ## Sandbox Mode 6 | 7 | Sandbox mode allows you to test sending emails without actually delivering emails and using your email quota. 8 | 9 | To send emails in sandbox mode, ensure the config key is set: 10 | 11 | config :sendgrid, 12 | api_key: "SENDGRID_API_KEY", 13 | sandbox_enable: true 14 | 15 | Optionally, you can use `SendGrid.Email.set_sandbox/2` to configure it per email. 16 | """ 17 | 18 | alias SendGrid.Email 19 | 20 | @mail_url "/v3/mail/send" 21 | 22 | @doc """ 23 | Sends the built email. 24 | 25 | ## Options 26 | 27 | * `:api_key` - API key to use with the request. 28 | 29 | ## Examples 30 | 31 | email = 32 | Email.build() 33 | |> Email.add_to("test@email.com") 34 | |> Email.put_from("test2@email.com") 35 | |> Email.put_subject("Hello from Elixir") 36 | |> Email.put_text("Sent with Elixir") 37 | 38 | :ok = Mail.send(email) 39 | """ 40 | @spec send(SendGrid.Email.t(), [SendGrid.api_key()]) :: 41 | :ok | {:error, [String.t()]} | {:error, String.t()} 42 | def send(%Email{} = email, opts \\ []) when is_list(opts) do 43 | case SendGrid.post(@mail_url, email, opts) do 44 | {:ok, %{status: status}} when status in [200, 202] -> :ok 45 | {:ok, %{body: body}} -> {:error, body["errors"]} 46 | _ -> {:error, "Unable to communicate with SendGrid API."} 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/sendgrid/marketing_campaigns/contacts/lists.ex: -------------------------------------------------------------------------------- 1 | defmodule SendGrid.Contacts.Lists do 2 | @moduledoc """ 3 | Module to interact with modifying email lists. 4 | 5 | See SendGrid's [Contact API Docs](https://sendgrid.com/docs/API_Reference/Web_API_v3/Marketing_Campaigns/contactdb.html) 6 | for more detail. 7 | """ 8 | 9 | @base_api_url "/v3/contactdb/lists" 10 | 11 | @doc """ 12 | Retrieves all email lists. 13 | 14 | ## Options 15 | 16 | * `:api_key` - API key to use with the request. 17 | """ 18 | @spec all([SendGrid.api_key()]) :: {:ok, [map()]} | {:error, any()} 19 | def all(opts \\ []) when is_list(opts) do 20 | with {:ok, %{status: 200, body: %{"lists" => lists}}} <- SendGrid.get(@base_api_url, opts) do 21 | {:ok, lists} 22 | end 23 | end 24 | 25 | @doc """ 26 | Creates an email list. 27 | 28 | ## Options 29 | 30 | * `:api_key` - API key to use with the request. 31 | """ 32 | @spec add(String.t(), [SendGrid.api_key()]) :: {:ok, map()} | {:error, :any} 33 | def add(list_name, opts \\ []) when is_list(opts) do 34 | with {:ok, %{status: 201, body: body}} <- 35 | SendGrid.post(@base_api_url, %{name: list_name}, opts) do 36 | {:ok, body} 37 | end 38 | end 39 | 40 | @doc """ 41 | Retrieves all recipients from an email list. 42 | 43 | ## Options 44 | 45 | * `:api_key` - API key to use with the request. 46 | * `:page` - Page to start at. **Defaults to 1**. 47 | * `:page_size` - Page size for pagination. **Defaults to 100**. 48 | """ 49 | @spec all_recipients(integer(), [SendGrid.api_key() | SendGrid.page() | SendGrid.page_size()]) :: 50 | {:ok[map()]} | {:error, any()} 51 | def all_recipients(list_id, opts \\ []) when is_list(opts) do 52 | page = Keyword.get(opts, :page, 1) 53 | page_size = Keyword.get(opts, :page_size, 100) 54 | query = [page: page, page_size: page_size] 55 | 56 | request_opts = 57 | opts 58 | |> Keyword.drop([:page, :page_size]) 59 | |> Keyword.merge(query: query) 60 | 61 | url = "#{@base_api_url}/#{list_id}/recipients" 62 | 63 | with {:ok, %{status: 200, body: %{"recipients" => recipients}}} <- 64 | SendGrid.get(url, request_opts) do 65 | {:ok, recipients} 66 | end 67 | end 68 | 69 | @doc """ 70 | Adds a recipient to an email list. 71 | 72 | ## Options 73 | 74 | * `:api_key` - API key to use with the request. 75 | """ 76 | @spec add_recipient(integer(), String.t(), [SendGrid.api_key()]) :: :ok | {:error, any()} 77 | def add_recipient(list_id, recipient_id, opts \\ []) when is_list(opts) do 78 | url = "#{@base_api_url}/#{list_id}/recipients/#{recipient_id}" 79 | 80 | with {:ok, %{status: 201}} <- SendGrid.post(url, %{}, opts) do 81 | :ok 82 | end 83 | end 84 | 85 | @doc """ 86 | Deletes a recipient from an email list. 87 | 88 | ## Options 89 | 90 | * `:api_key` - API key to use with the request. 91 | """ 92 | @spec delete_recipient(integer, String.t(), [SendGrid.api_key()]) :: :ok | {:error, any()} 93 | def delete_recipient(list_id, recipient_id, opts \\ []) when is_list(opts) do 94 | url = "#{@base_api_url}/#{list_id}/recipients/#{recipient_id}" 95 | 96 | with {:ok, %{status: 204}} <- SendGrid.delete(url, opts) do 97 | :ok 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/sendgrid/marketing_campaigns/contacts/recipient.ex: -------------------------------------------------------------------------------- 1 | defmodule SendGrid.Contacts.Recipient do 2 | @moduledoc """ 3 | Struct to help with creating a recipient. 4 | """ 5 | 6 | alias SendGrid.Contacts.Recipient 7 | 8 | @enforce_keys [:email] 9 | defstruct [:custom_fields, :email] 10 | 11 | @type t :: %Recipient{ 12 | email: String.t(), 13 | custom_fields: nil | map() 14 | } 15 | 16 | @doc """ 17 | Builds a Repient to be used in `SendGrid.Contacts.Recipents`. 18 | """ 19 | @spec build(String.t(), map()) :: t() 20 | def build(email, custom_fields \\ %{}) when is_map(custom_fields) do 21 | %Recipient{ 22 | email: email, 23 | custom_fields: custom_fields 24 | } 25 | end 26 | 27 | defimpl Jason.Encoder do 28 | def encode(%Recipient{email: email, custom_fields: fields}, opts) when is_map(fields) do 29 | Jason.Encode.map(Map.merge(fields, %{email: email}), opts) 30 | end 31 | 32 | def encode(%Recipient{email: email, custom_fields: nil}, opts) do 33 | Jason.Encode.map(%{email: email}, opts) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/sendgrid/marketing_campaigns/contacts/recipients.ex: -------------------------------------------------------------------------------- 1 | defmodule SendGrid.Contacts.Recipients do 2 | @moduledoc """ 3 | Module to interact with modifying contacts. 4 | 5 | See SendGrid's [Contact API Docs](https://sendgrid.com/docs/API_Reference/Web_API_v3/Marketing_Campaigns/contactdb.html) 6 | for more detail. 7 | """ 8 | 9 | alias SendGrid.Contacts.Recipient 10 | 11 | @base_api_url "/v3/contactdb/recipients" 12 | 13 | @doc """ 14 | Adds a contact to the contacts list available in Marketing Campaigns. 15 | 16 | When adding a contact, an email address must provided at a minimum. Custom 17 | fields that have already been created can added as well. 18 | 19 | ## Options 20 | 21 | * `:api_key` - API key to use with the request. 22 | 23 | ## Examples 24 | 25 | alias SendGrid.Contacts.Recipient 26 | 27 | {:ok, recipient_id} = add(Recipient.build("test@example.com", %{"name" => "John Doe"})) 28 | 29 | {:ok, recipient_id} = add(Recipient.build("test@example.com")) 30 | """ 31 | @spec add(Recipient.t(), [SendGrid.api_key()]) :: 32 | {:ok, String.t()} | {:error, [String.t(), ...]} 33 | def add(%Recipient{} = recipient, opts \\ []) when is_list(opts) do 34 | with {:ok, response} <- SendGrid.post(@base_api_url, recipient, opts) do 35 | handle_recipient_result(response) 36 | end 37 | end 38 | 39 | # Handles the result when errors are present. 40 | defp handle_recipient_result(%{body: %{"error_count" => count} = body}) when count > 0 do 41 | errors = Enum.map(body["errors"], & &1["message"]) 42 | 43 | {:error, errors} 44 | end 45 | 46 | # Handles the result when it's valid. 47 | defp handle_recipient_result(%{body: %{"persisted_recipients" => [recipient_id]}}) do 48 | {:ok, recipient_id} 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/sendgrid/personalization.ex: -------------------------------------------------------------------------------- 1 | defmodule SendGrid.Personalization do 2 | @moduledoc """ 3 | Personalizations are used by SendGrid v3 API to identify who should receive 4 | the email as well as specifics about how you would like the email to be handled. 5 | """ 6 | 7 | alias SendGrid.{Email, Personalization} 8 | 9 | defstruct to: nil, 10 | cc: nil, 11 | bcc: nil, 12 | subject: nil, 13 | substitutions: nil, 14 | custom_args: nil, 15 | dynamic_template_data: nil, 16 | send_at: nil, 17 | headers: nil 18 | 19 | @type t :: %Personalization{ 20 | to: nil | [Email.recipient()], 21 | cc: nil | [Email.recipient()], 22 | bcc: nil | [Email.recipient()], 23 | subject: nil | String.t(), 24 | substitutions: nil | Email.substitutions(), 25 | custom_args: nil | Email.custom_args(), 26 | dynamic_template_data: nil | Email.dynamic_template_data(), 27 | send_at: nil | non_neg_integer(), 28 | headers: nil | Email.headers() 29 | } 30 | 31 | defimpl Jason.Encoder do 32 | def encode(%Personalization{} = personalization, opts) do 33 | params = 34 | personalization 35 | |> Map.take( 36 | ~w(to cc bcc subject substitutions custom_args dynamic_template_data send_at headers)a 37 | ) 38 | |> Enum.filter(fn {_key, v} -> v != nil && v != [] end) 39 | |> Enum.into(%{}) 40 | 41 | Jason.Encode.map(params, opts) 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/sendgrid/response.ex: -------------------------------------------------------------------------------- 1 | defmodule SendGrid.Response do 2 | @moduledoc """ 3 | Represents the result from performing a request. 4 | """ 5 | 6 | alias SendGrid.Response 7 | 8 | @type t :: %Response{ 9 | body: map(), 10 | headers: Keyword.t(), 11 | status: pos_integer() 12 | } 13 | 14 | @enforce_keys ~w(body headers status)a 15 | defstruct ~w(body headers status)a 16 | end 17 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule SendGrid.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :sendgrid, 6 | version: "2.0.0", 7 | elixir: "~> 1.4", 8 | package: package(), 9 | compilers: compilers(Mix.env), 10 | description: description(), 11 | source_url: project_url(), 12 | homepage_url: project_url(), 13 | elixirc_paths: elixirc_paths(Mix.env), 14 | build_embedded: Mix.env == :prod, 15 | start_permanent: Mix.env == :prod, 16 | deps: deps(), 17 | xref: [exclude: [Phoenix.View]], 18 | preferred_cli_env: [ 19 | dialyzer: :test, 20 | "test.integration": :test 21 | ], 22 | aliases: aliases() 23 | ] 24 | end 25 | 26 | def application do 27 | [ 28 | extra_applications: [ 29 | :logger 30 | ] 31 | ] 32 | end 33 | 34 | # Use Phoenix compiler depending on environment. 35 | defp compilers(:test), do: [:phoenix] ++ Mix.compilers() 36 | defp compilers(_), do: Mix.compilers() 37 | 38 | # Specifies which paths to compile per environment. 39 | defp elixirc_paths(:test), do: ["lib", "test/support"] 40 | defp elixirc_paths(_), do: ["lib"] 41 | 42 | defp deps do 43 | [ 44 | {:dialyxir, "~> 1.0.0-rc.4", only: [:dev, :test], runtime: false}, 45 | {:earmark, "~> 1.2", only: :dev}, 46 | {:ex_doc, "~> 0.19", only: :dev}, 47 | {:jason, "~> 1.1"}, 48 | {:phoenix, "~> 1.2", only: :test}, 49 | {:phoenix_html, "~> 2.9", only: :test}, 50 | {:tesla, "~> 1.2"} 51 | ] 52 | end 53 | 54 | defp description do 55 | """ 56 | A wrapper for SendGrid's API to create composable emails. 57 | """ 58 | end 59 | 60 | defp project_url do 61 | """ 62 | https://github.com/alexgaribay/sendgrid_elixir 63 | """ 64 | end 65 | 66 | defp package do 67 | [ 68 | files: ["lib", "mix.exs", "LICENSE", "README.md"], 69 | maintainers: ["Alex Garibay"], 70 | licenses: ["MIT"], 71 | links: %{"GitHub" => project_url()} 72 | ] 73 | end 74 | 75 | defp aliases do 76 | [ 77 | test: [ 78 | "test --exclude integration" 79 | ], 80 | "test.integration": [ 81 | "test --only integration" 82 | ] 83 | ] 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "certifi": {:hex, :certifi, "0.7.0", "861a57f3808f7eb0c2d1802afeaae0fa5de813b0df0979153cbafcd853ababaf", [:rebar3], []}, 3 | "dialyxir": {:hex, :dialyxir, "1.0.0-rc.4", "71b42f5ee1b7628f3e3a6565f4617dfb02d127a0499ab3e72750455e986df001", [:mix], [{:erlex, "~> 0.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"}, 5 | "erlex": {:hex, :erlex, "0.1.6", "c01c889363168d3fdd23f4211647d8a34c0f9a21ec726762312e08e083f3d47e", [:mix], [], "hexpm"}, 6 | "ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, 7 | "floki": {:hex, :floki, "0.17.2", "81b3a39d85f5cae39c8da16236ce152f7f8f50faf84b480ba53351d7e96ca6ca", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, optional: false]}]}, 8 | "hackney": {:hex, :hackney, "1.6.5", "8c025ee397ac94a184b0743c73b33b96465e85f90a02e210e86df6cbafaa5065", [:rebar3], [{:certifi, "0.7.0", [hex: :certifi, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]}, 9 | "httpoison": {:hex, :httpoison, "0.11.0", "b9240a9c44fc46fcd8618d17898859ba09a3c1b47210b74316c0ffef10735e76", [:mix], [{:hackney, "~> 1.6.3", [hex: :hackney, optional: false]}]}, 10 | "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []}, 11 | "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, 12 | "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 13 | "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, 14 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, 15 | "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [:mix], []}, 16 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, 17 | "mochiweb": {:hex, :mochiweb, "2.15.0", "e1daac474df07651e5d17cc1e642c4069c7850dc4508d3db7263a0651330aacc", [:rebar3], []}, 18 | "nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"}, 19 | "phoenix": {:hex, :phoenix, "1.2.4", "4172479b5e21806a5e4175b54820c239e0d4effb0b07912e631aa31213a05bae", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, optional: false]}, {:plug, "~> 1.4 or ~> 1.3.3 or ~> 1.2.4 or ~> 1.1.8 or ~> 1.0.5", [hex: :plug, optional: false]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: false]}]}, 20 | "phoenix_html": {:hex, :phoenix_html, "2.9.3", "1b5a2122cbf743aa242f54dced8a4f1cc778b8bd304f4b4c0043a6250c58e258", [:mix], [{:plug, "~> 1.0", [hex: :plug, optional: false]}]}, 21 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [:mix], []}, 22 | "plug": {:hex, :plug, "1.3.5", "7503bfcd7091df2a9761ef8cecea666d1f2cc454cbbaf0afa0b6e259203b7031", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]}, 23 | "poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], []}, 24 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []}, 25 | "ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5"}, 26 | "tesla": {:hex, :tesla, "1.2.0", "9e2469c1bcdb0cc8fe5fd3e9208432f3fee8e439a44f639d8ce72bcccd6f3566", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, 27 | } 28 | -------------------------------------------------------------------------------- /test/email_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SendGrid.Email.Test do 2 | use ExUnit.Case, async: true 3 | 4 | alias SendGrid.{Email, Personalization} 5 | 6 | @email "test@email.com" 7 | @name "John Doe" 8 | 9 | test "build/0" do 10 | assert Email.build() == %Email{} 11 | end 12 | 13 | test "add_to/2" do 14 | email = Email.add_to(Email.build(), @email) 15 | assert email.to == [%{email: @email}] 16 | end 17 | 18 | test "add_to/3" do 19 | email = Email.add_to(Email.build(), @email, @name) 20 | assert email.to == [%{email: @email, name: @name}] 21 | end 22 | 23 | test "add_to with multiple addresses" do 24 | email = 25 | Email.build() 26 | |> Email.add_to(@email) 27 | |> Email.add_to(@email, @name) 28 | 29 | assert email.to == [%{email: @email}, %{email: @email, name: @name}] 30 | end 31 | 32 | test "put_from/2" do 33 | email = Email.put_from(Email.build(), @email) 34 | assert email.from == %{email: @email} 35 | end 36 | 37 | test "put_from/3" do 38 | email = Email.put_from(Email.build(), @email, @name) 39 | assert email.from == %{email: @email, name: @name} 40 | end 41 | 42 | test "add_cc/2" do 43 | email = Email.add_cc(Email.build(), @email) 44 | assert email.cc == [%{email: @email}] 45 | end 46 | 47 | test "add_cc/3" do 48 | email = Email.add_cc(Email.build(), @email, @name) 49 | assert email.cc == [%{email: @email, name: @name}] 50 | end 51 | 52 | test "add_cc with multiple addresses" do 53 | email = 54 | Email.build() 55 | |> Email.add_cc(@email) 56 | |> Email.add_cc(@email, @name) 57 | 58 | assert email.cc == [%{email: @email}, %{email: @email, name: @name}] 59 | end 60 | 61 | test "add_bcc/2" do 62 | email = Email.add_bcc(Email.build(), @email) 63 | assert email.bcc == [%{email: @email}] 64 | end 65 | 66 | test "add_bcc/3" do 67 | email = Email.add_bcc(Email.build(), @email, @name) 68 | assert email.bcc == [%{email: @email, name: @name}] 69 | end 70 | 71 | test "add_bcc with multiple addresses" do 72 | email = 73 | Email.build() 74 | |> Email.add_bcc(@email) 75 | |> Email.add_bcc(@email, @name) 76 | 77 | assert email.bcc == [%{email: @email}, %{email: @email, name: @name}] 78 | end 79 | 80 | test "put_reply_to/2" do 81 | email = Email.put_reply_to(Email.build(), @email) 82 | assert email.reply_to == %{email: @email} 83 | end 84 | 85 | test "put_reply_to/3" do 86 | email = Email.put_reply_to(Email.build(), @email, @name) 87 | assert email.reply_to == %{email: @email, name: @name} 88 | end 89 | 90 | test "put_subject/2" do 91 | subject = "Test Subject" 92 | email = Email.put_subject(Email.build(), subject) 93 | assert email.subject == subject 94 | end 95 | 96 | test "put_text/2" do 97 | text = "Some Text" 98 | email = Email.put_text(Email.build(), text) 99 | assert email.content == [%{type: "text/plain", value: text}] 100 | end 101 | 102 | test "put_html/2" do 103 | html = "

Some Text

" 104 | email = Email.put_html(Email.build(), html) 105 | assert email.content == [%{type: "text/html", value: html}] 106 | end 107 | 108 | test "put multiple content types" do 109 | text = "Some Text" 110 | html = "

Some Text

" 111 | 112 | email = 113 | Email.build() 114 | |> Email.put_text(text) 115 | |> Email.put_html(html) 116 | 117 | assert email.content == [ 118 | %{type: "text/plain", value: text}, 119 | %{type: "text/html", value: html} 120 | ] 121 | end 122 | 123 | test "text content comes before html" do 124 | text = "Some Text" 125 | html = "

Some Text

" 126 | 127 | email = 128 | Email.build() 129 | |> Email.put_html(html) 130 | |> Email.put_text(text) 131 | 132 | assert email.content == [ 133 | %{type: "text/plain", value: text}, 134 | %{type: "text/html", value: html} 135 | ] 136 | end 137 | 138 | test "add_header/3" do 139 | header_key = "SOME_KEY" 140 | header_value = "SOME_VALUE" 141 | email = Email.add_header(Email.build(), header_key, header_value) 142 | assert email.headers == %{header_key => header_value} 143 | end 144 | 145 | test "put_template/2" do 146 | template_id = "some_unique_id" 147 | email = Email.put_template(Email.build(), template_id) 148 | assert email.template_id == template_id 149 | end 150 | 151 | test "add_substitution/3" do 152 | email = Email.add_substitution(Email.build(), "-someValue-", "Cool") 153 | assert email.substitutions == %{"-someValue-" => "Cool"} 154 | end 155 | 156 | test "add_subtitution/3 x2" do 157 | email = 158 | Email.build() 159 | |> Email.add_substitution("-someValue-", "Cool") 160 | |> Email.add_substitution("-newValue-", "Panda") 161 | 162 | assert email.substitutions == %{"-someValue-" => "Cool", "-newValue-" => "Panda"} 163 | end 164 | 165 | test "add_custom_arg/3" do 166 | email = Email.add_custom_arg(Email.build(), "unique_user_id", "abc123") 167 | assert email.custom_args == %{"unique_user_id" => "abc123"} 168 | end 169 | 170 | test "add_custom_arg/3 x2" do 171 | email = 172 | Email.build() 173 | |> Email.add_custom_arg("unique_user_id", "abc123") 174 | |> Email.add_custom_arg("template_name", "welcome-user") 175 | 176 | assert email.custom_args == %{"unique_user_id" => "abc123", "template_name" => "welcome-user"} 177 | end 178 | 179 | test "add_custom_arg/3 does not create duplicate keys" do 180 | email = 181 | Email.build() 182 | |> Email.add_custom_arg("unique_user_id", "abc123") 183 | |> Email.add_custom_arg("template_name", "welcome-user") 184 | |> Email.add_custom_arg("template_name", "new_template") 185 | 186 | assert email.custom_args == %{"unique_user_id" => "abc123", "template_name" => "new_template"} 187 | end 188 | 189 | test "put_send_at/2" do 190 | time = 123_456_789 191 | email = Email.put_send_at(Email.build(), time) 192 | assert email.send_at == time 193 | end 194 | 195 | describe "add_attachemnt/2" do 196 | test "adds a single attachemnt" do 197 | attachment = %{ 198 | content: "somebase64encodedstring", 199 | type: "image/jpeg", 200 | filename: "testing.jpg" 201 | } 202 | 203 | email = Email.add_attachment(Email.build(), attachment) 204 | assert email.attachments == [attachment] 205 | assert Enum.count(email.attachments) == 1 206 | end 207 | 208 | test "appends to attachment list" do 209 | attachment1 = %{ 210 | content: "somebase64encodedstring", 211 | type: "image/jpeg", 212 | filename: "testing.jpg" 213 | } 214 | 215 | attachment2 = %{ 216 | content: "somebase64encodedstring2", 217 | type: "image/png", 218 | filename: "testing2.jpg" 219 | } 220 | 221 | email = 222 | Email.build() 223 | |> Email.add_attachment(attachment1) 224 | |> Email.add_attachment(attachment2) 225 | 226 | assert email.attachments == [attachment1, attachment2] 227 | assert Enum.count(email.attachments) == 2 228 | end 229 | end 230 | 231 | defmodule EmailView do 232 | use Phoenix.View, root: "test/support/templates", namespace: SendGrid.Email.Test 233 | end 234 | 235 | describe "put_phoenix_layout/2" do 236 | test "works with html layouts" do 237 | result = 238 | Email.build() 239 | |> Email.put_phoenix_layout({SendGrid.Email.Test.EmailView, "layout.html"}) 240 | 241 | assert result.__phoenix_layout__ == %{html: {SendGrid.Email.Test.EmailView, "layout.html"}} 242 | end 243 | 244 | test "works with text layouts" do 245 | result = 246 | Email.build() 247 | |> Email.put_phoenix_layout({SendGrid.Email.Test.EmailView, "layout.txt"}) 248 | 249 | assert result.__phoenix_layout__ == %{text: {SendGrid.Email.Test.EmailView, "layout.txt"}} 250 | end 251 | 252 | test "works with setting both a text and an html layout" do 253 | result = 254 | Email.build() 255 | |> Email.put_phoenix_layout({SendGrid.Email.Test.EmailView, "layout.txt"}) 256 | |> Email.put_phoenix_layout({SendGrid.Email.Test.EmailView, "layout.html"}) 257 | 258 | expected = %{ 259 | html: {SendGrid.Email.Test.EmailView, "layout.html"}, 260 | text: {SendGrid.Email.Test.EmailView, "layout.txt"} 261 | } 262 | 263 | assert result.__phoenix_layout__ == expected 264 | end 265 | 266 | test "works with an atom as a layout" do 267 | result = 268 | Email.build() 269 | |> Email.put_phoenix_layout({SendGrid.Email.Test.EmailView, :layout}) 270 | 271 | expected = %{ 272 | html: {SendGrid.Email.Test.EmailView, "layout.html"}, 273 | text: {SendGrid.Email.Test.EmailView, "layout.txt"} 274 | } 275 | 276 | assert result.__phoenix_layout__ == expected 277 | end 278 | end 279 | 280 | test "put_phoenix_view/2" do 281 | result = 282 | Email.build() 283 | |> Email.put_phoenix_view(SendGrid.Email.Test.EmailView) 284 | 285 | assert %Email{__phoenix_view__: SendGrid.Email.Test.EmailView} = result 286 | end 287 | 288 | describe "put_phoenix_template/2" do 289 | test "renders templates with explicit extensions" do 290 | # HTML 291 | result = 292 | Email.build() 293 | |> Email.put_phoenix_view(SendGrid.Email.Test.EmailView) 294 | |> Email.put_phoenix_template("test.html", test: "awesome") 295 | 296 | assert %Email{content: [%{type: "text/html", value: "

awesome

"}]} = result 297 | 298 | # Text 299 | result = 300 | Email.build() 301 | |> Email.put_phoenix_view(SendGrid.Email.Test.EmailView) 302 | |> Email.put_phoenix_template("test.txt", test: "awesome") 303 | 304 | assert %Email{content: [%{type: "text/plain", value: "awesome"}]} = result 305 | end 306 | 307 | test "renders templates with implicit extensions" do 308 | result = 309 | Email.build() 310 | |> Email.put_phoenix_template(:test, test: "awesome") 311 | 312 | assert %Email{ 313 | content: [ 314 | %{type: "text/plain", value: "awesome"}, 315 | %{type: "text/html", value: "

awesome

"} 316 | ] 317 | } = result 318 | end 319 | 320 | test "raises when a template doesn't exist for implicit extensions" do 321 | assert_raise Phoenix.Template.UndefinedError, fn -> 322 | Email.put_phoenix_template(Email.build(), :test2) 323 | end 324 | end 325 | 326 | test "renders using the configured phoenix view" do 327 | result = 328 | Email.build() 329 | |> Email.put_phoenix_template("test.txt", test: "awesome") 330 | 331 | assert %Email{content: [%{type: "text/plain", value: "awesome"}]} = result 332 | end 333 | 334 | test "renders in a text layout" do 335 | result = 336 | Email.build() 337 | |> Email.put_phoenix_layout({SendGrid.Email.Test.EmailView, :layout}) 338 | |> Email.put_phoenix_template("test.txt", test: "awesome") 339 | 340 | assert Enum.at(result.content, 0).value =~ "TEXT LAYOUT" 341 | assert Enum.at(result.content, 0).value =~ "awesome" 342 | end 343 | 344 | test "renders in an html layout" do 345 | result = 346 | Email.build() 347 | |> Email.put_phoenix_layout({SendGrid.Email.Test.EmailView, :layout}) 348 | |> Email.put_phoenix_template("test.html", test: "awesome") 349 | 350 | assert Enum.at(result.content, 0).value =~ "HTML LAYOUT" 351 | assert Enum.at(result.content, 0).value =~ "awesome" 352 | end 353 | end 354 | 355 | test "set_set_sandbox/2" do 356 | email = Email.set_sandbox(Email.build(), true) 357 | assert email.sandbox 358 | email = Email.set_sandbox(Email.build(), false) 359 | refute email.sandbox 360 | end 361 | 362 | test "to_personalization/1" do 363 | email = 364 | Email.build() 365 | |> Email.add_to(@email) 366 | |> Email.add_cc(@email) 367 | 368 | assert Email.to_personalization(email) == %Personalization{ 369 | to: [%{email: @email}], 370 | cc: [%{email: @email}] 371 | } 372 | end 373 | 374 | test "add_personalization/2" do 375 | personalization_1 = %Personalization{send_at: 1} 376 | personalization_2 = %Personalization{send_at: 2} 377 | 378 | email = Email.add_personalization(Email.build(), personalization_1) 379 | assert email.personalizations == [personalization_1] 380 | email = Email.add_personalization(email, personalization_2) 381 | assert email.personalizations == [personalization_1, personalization_2] 382 | end 383 | end 384 | -------------------------------------------------------------------------------- /test/mail_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SendGrid.MailTest do 2 | use ExUnit.Case 3 | alias SendGrid.{Email, Mail} 4 | 5 | @moduletag integration: true 6 | 7 | describe "send" do 8 | test "with an Email containing no personalization" do 9 | result = 10 | Email.build() 11 | |> Email.add_to(Application.get_env(:sendgrid, :test_address)) 12 | |> Email.put_from(Application.get_env(:sendgrid, :test_address)) 13 | |> Email.put_subject("Test") 14 | |> Email.put_text("123") 15 | |> Email.put_html("

123

") 16 | |> Email.add_header("TEST", "FOO") 17 | |> Mail.send() 18 | 19 | assert :ok == result 20 | end 21 | 22 | test "with an Email containing multiple personalizations" do 23 | base = 24 | Email.build() 25 | |> Email.put_from(Application.get_env(:sendgrid, :test_address)) 26 | |> Email.put_subject("Test") 27 | |> Email.put_text("123") 28 | |> Email.put_html("

123

") 29 | 30 | email = 31 | Enum.reduce(1..5, base, fn x, email -> 32 | personalization = 33 | Email.build() 34 | |> Email.add_to(Application.get_env(:sendgrid, :test_address)) 35 | |> Email.put_subject("Test #{x}") 36 | |> Email.add_header("TEST#{x}", "FOO") 37 | |> Email.to_personalization() 38 | 39 | Email.add_personalization(email, personalization) 40 | end) 41 | 42 | assert :ok == Mail.send(email) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/support/email_view.ex: -------------------------------------------------------------------------------- 1 | defmodule SendGrid.EmailView do 2 | use Phoenix.View, 3 | root: "test/support/templates", 4 | namespace: SendGrid 5 | end 6 | -------------------------------------------------------------------------------- /test/support/templates/email/layout.html.eex: -------------------------------------------------------------------------------- 1 | HTML LAYOUT 2 | <%= render @view_module, @view_template, assigns %> -------------------------------------------------------------------------------- /test/support/templates/email/layout.txt.eex: -------------------------------------------------------------------------------- 1 | TEXT LAYOUT 2 | <%= render @view_module, @view_template, assigns %> -------------------------------------------------------------------------------- /test/support/templates/email/test.html.eex: -------------------------------------------------------------------------------- 1 |

<%= @test %>

-------------------------------------------------------------------------------- /test/support/templates/email/test.txt.eex: -------------------------------------------------------------------------------- 1 | <%= @test %> -------------------------------------------------------------------------------- /test/support/templates/email/test2.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexgaribay/sendgrid_elixir/7c722cfcb554677ff3128e8c207f55e5373f1a0e/test/support/templates/email/test2.txt -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------