├── .circleci └── config.yml ├── .formatter.exs ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── config └── config.exs ├── lib └── bamboo │ ├── postmark_adapter.ex │ └── postmark_helper.ex ├── mix.exs ├── mix.lock └── test ├── lib └── bamboo │ └── postmark_adapter_test.exs ├── support └── attachment.txt └── test_helper.exs /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/bamboo_postmark 5 | docker: 6 | - image: elixir:1.7.4 7 | steps: 8 | - checkout 9 | - run: mix local.hex --force 10 | - run: mix local.rebar 11 | - run: mix deps.get 12 | - run: mix test 13 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.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 | bamboo_postmark-*.tar 24 | 25 | # Temporary files, for example, from 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 | ## v1.0.0 - 2021-04-30 9 | 10 | ### Changes 11 | * Change return of deliver/2 to work with Bamboo v2.0 12 | * Require Bamboo 2.0.0 or above 13 | 14 | ## v0.7.0 - 2020-12-13 15 | 16 | ### Fixes/Enhancements 17 | 18 | * Support inline attachments ([#34]) 19 | 20 | [#34]: https://github.com/pablo-co/bamboo_postmark/pull/34 21 | 22 | ## v0.6.1 - 2020-12-13 23 | 24 | ### Fixes/Enhancements 25 | 26 | * Pass attachments as params to Postmark ([#31]) 27 | 28 | [#31]: https://github.com/pablo-co/bamboo_postmark/pull/31 29 | 30 | ## v0.6.0 - 2019-06-19 31 | 32 | ### New Additions 33 | 34 | * Allow configuring API key with {:system, "ENVVAR"} ([#26]) 35 | 36 | [#26]: https://github.com/pablo-co/bamboo_postmark/pull/26 37 | 38 | ## v0.5.0 - 2019-02-11 39 | 40 | ### New Additions 41 | 42 | * Make JSON library configurable ([#22]) 43 | 44 | [#22]: https://github.com/pablo-co/bamboo_postmark/pull/22 45 | 46 | ## v0.4.2 - 2018-01-23 47 | 48 | ### Fixes/Enhancements 49 | 50 | * Relax bamboo and hackney library requirements ([#18]) 51 | * Fix bug when leaving default `template_model` value in `PostmarkHelper.template/3` ([#17]) 52 | * Fix code style issues and compiler warnings ([#16]) 53 | 54 | [#16]: https://github.com/pablo-co/bamboo_postmark/pull/16 55 | [#17]: https://github.com/pablo-co/bamboo_postmark/pull/17 56 | [#18]: https://github.com/pablo-co/bamboo_postmark/pull/18 57 | 58 | ## v0.4.1 - 2017-06-07 59 | 60 | ### Fixes/Enhancements 61 | 62 | * Return body in response of PostmarkAdapter.deliver ([#14]) 63 | 64 | [#14]: https://github.com/pablo-co/bamboo_postmark/pull/14 65 | 66 | ## v0.4.0 - 2017-05-23 67 | 68 | ### New Additions 69 | 70 | * Allow configuration of request options ([#12]) 71 | 72 | [#12]: https://github.com/pablo-co/bamboo_postmark/pull/12 73 | 74 | ## v0.3.0 - 2017-05-06 75 | 76 | ### New Additions 77 | 78 | * Support passing custom params to Postmark ([#9]) 79 | 80 | ### Fixes/Enhancements 81 | 82 | * Fix Elixir 1.4 warnings and deprecations ([#7]) 83 | 84 | [#9]: https://github.com/pablo-co/bamboo_postmark/pull/9 85 | [#7]: https://github.com/pablo-co/bamboo_postmark/pull/7 86 | 87 | ## v0.2.0 - 2016-12-29 88 | 89 | ### New Additions 90 | 91 | * Add support for tagging emails ([#5]) 92 | 93 | ### Fixes/Enhancements 94 | 95 | * Relax Bamboo version from library dependencies ([#6]) 96 | 97 | [#5]: https://github.com/pablo-co/bamboo_postmark/pull/5 98 | [#6]: https://github.com/pablo-co/bamboo_postmark/pull/6 99 | 100 | ## v0.1.0 - 2016-09-02 101 | 102 | * Initial public release. 103 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Pablo Cárdenas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bamboo.PostmarkAdapter 2 | 3 | [![CircleCI](https://circleci.com/gh/pablo-co/bamboo_postmark.svg?style=svg)](https://circleci.com/gh/pablo-co/bamboo_postmark) 4 | [![Module Version](https://img.shields.io/hexpm/v/bamboo_postmark.svg)](https://hex.pm/packages/bamboo_postmark) 5 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/bamboo_postmark/) 6 | [![Total Download](https://img.shields.io/hexpm/dt/bamboo_postmark.svg)](https://hex.pm/packages/bamboo_postmark) 7 | [![License](https://img.shields.io/hexpm/l/bamboo_postmark.svg)](https://github.com/pablo-co/bamboo_postmark/blob/master/LICENSE.md) 8 | [![Last Updated](https://img.shields.io/github/last-commit/pablo-co/bamboo_postmark.svg)](https://github.com/pablo-co/bamboo_postmark/commits/master) 9 | 10 | A [Postmark](https://postmarkapp.com/) adapter for the [Bamboo](https://github.com/thoughtbot/bamboo) email library. 11 | 12 | ## Installation 13 | 14 | The package can be installed by adding `:bamboo_postmark` to your list of 15 | dependencies in `mix.exs`: 16 | 17 | ```elixir 18 | def deps do 19 | # Get from hex 20 | [ 21 | {:bamboo_postmark, "~> 1.0"} 22 | ] 23 | 24 | # Or use the latest from master 25 | [ 26 | {:bamboo_postmark, github: "pablo-co/bamboo_postmark"} 27 | ] 28 | end 29 | ``` 30 | 31 | Add your Postmark API key to your config. You can find this key as `Server API 32 | token` under the `Credentials` tab in each Postmark server. 33 | 34 | ```elixir 35 | # In your configuration file: 36 | # * General configuration: config/config.exs 37 | # * Recommended production only: config/prod.exs 38 | 39 | config :my_app, MyApp.Mailer, 40 | adapter: Bamboo.PostmarkAdapter, 41 | api_key: "my_api_key" 42 | # Or if you want to use an ENV variable: 43 | # api_key: {:system, "POSTMARK_API_KEY"} 44 | ``` 45 | 46 | Follow Bamboo [Getting Started Guide](https://github.com/thoughtbot/bamboo#getting-started). 47 | 48 | ## Using templates 49 | 50 | The Postmark adapter provides a helper module for setting the template of an 51 | email. 52 | 53 | ```elixir 54 | defmodule MyApp.Mail do 55 | import Bamboo.PostmarkHelper 56 | 57 | def some_email do 58 | email 59 | |> template("id_of_template", 60 | %{name: "John Doe", confirm_link: "http://www.link.com"}) 61 | end 62 | end 63 | ``` 64 | 65 | #### Exception Warning 66 | 67 | Postmark templates include a subject, HTML body and text body and thus these shouldn't be included in the email as they will raise an API exception. 68 | 69 | ```elixir 70 | email 71 | |> template("id", %{value: "Some value"}) 72 | |> subject("Will raise exception") 73 | |> html_body("

Will raise exception

") 74 | |> text_body("Will raise exception") 75 | ``` 76 | 77 | ## Tagging emails 78 | 79 | The Postmark adapter provides a helper module for tagging emails. 80 | 81 | ```elixir 82 | defmodule MyApp.Mail do 83 | import Bamboo.PostmarkHelper 84 | 85 | def some_email do 86 | email 87 | |> tag("some-tag") 88 | end 89 | end 90 | ``` 91 | 92 | ## Sending extra parameters 93 | 94 | You can send other extra parameters to Postmark with the `put_param` helper. 95 | 96 | See Postmark's API for a complete list of parameters supported. 97 | 98 | ```elixir 99 | email 100 | |> put_param("TrackLinks", "HtmlAndText") 101 | |> put_param("TrackOpens", true) 102 | |> put_param("Attachments", [ 103 | %{ 104 | Name: "file.txt", 105 | Content: "/some/file.txt" |> File.read!() |> Base.encode64(), 106 | ContentType: "txt" 107 | } 108 | ]) 109 | ``` 110 | 111 | ## Changing the underlying request configuration 112 | 113 | You can specify the options that are passed to the underlying HTTP client 114 | [hackney](https://github.com/benoitc/hackney) by using the `request_options` key 115 | in the configuration. 116 | 117 | ### Example 118 | 119 | ```elixir 120 | config :my_app, MyApp.Mailer, 121 | adapter: Bamboo.PostmarkAdapter, 122 | api_key: "my_api_key", 123 | request_options: [recv_timeout: 10_000] 124 | ``` 125 | 126 | ## JSON support 127 | 128 | Bamboo comes with JSON support out of the box, see [Bamboo JSON support](https://github.com/thoughtbot/bamboo#json-support). 129 | 130 | ## Copyright and License 131 | 132 | Copyright (c) 2016 Pablo Cárdenas 133 | 134 | This library is released under the MIT License. See the [LICENSE.md](./LICENSE.md) file. 135 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :bamboo, :json_library, Poison 4 | -------------------------------------------------------------------------------- /lib/bamboo/postmark_adapter.ex: -------------------------------------------------------------------------------- 1 | defmodule Bamboo.PostmarkAdapter do 2 | @moduledoc """ 3 | Sends email using Postmarks's API. 4 | 5 | Use this adapter to send emails through Postmark's API. Requires that an API 6 | key is set in the config. 7 | 8 | ## Example 9 | 10 | # In config/config.exs, or config.prod.exs, etc. 11 | config :my_app, MyApp.Mailer, 12 | adapter: Bamboo.PostmarkAdapter, 13 | api_key: "my_api_key" or {:system, "POSTMARK_API_KEY"} 14 | 15 | """ 16 | 17 | @behaviour Bamboo.Adapter 18 | 19 | @default_base_uri "https://api.postmarkapp.com" 20 | @send_email_path "email" 21 | @send_email_template_path "email/withTemplate" 22 | 23 | import Bamboo.ApiError, only: [build_api_error: 1] 24 | 25 | def deliver(email, config) do 26 | api_key = get_key(config) 27 | params = email |> convert_to_postmark_params() |> json_library().encode!() 28 | uri = [base_uri(), "/", api_path(email)] 29 | 30 | case :hackney.post(uri, headers(api_key), params, options(config)) do 31 | {:ok, status, _headers, response} when status > 299 -> 32 | {:error, build_api_error(%{params: params, response: response})} 33 | 34 | {:ok, status, headers, response} -> 35 | {:ok, %{status_code: status, headers: headers, body: response}} 36 | 37 | {:error, reason} -> 38 | {:error, build_api_error(%{message: inspect(reason)})} 39 | end 40 | end 41 | 42 | def handle_config(config) do 43 | # build the api key - will raise if there are errors 44 | Map.merge(config, %{api_key: get_key(config)}) 45 | end 46 | 47 | @doc false 48 | def supports_attachments?, do: true 49 | 50 | defp get_key(config) do 51 | api_key = 52 | case Map.get(config, :api_key) do 53 | {:system, var} -> System.get_env(var) 54 | key -> key 55 | end 56 | 57 | if api_key in [nil, ""] do 58 | raise_api_key_error(config) 59 | else 60 | api_key 61 | end 62 | end 63 | 64 | def json_library do 65 | Bamboo.json_library() 66 | end 67 | 68 | defp raise_api_key_error(config) do 69 | raise ArgumentError, """ 70 | There was no API key set for the Postmark adapter. 71 | * Here are the config options that were passed in: 72 | #{inspect config} 73 | """ 74 | end 75 | 76 | defp convert_to_postmark_params(email) do 77 | email 78 | |> email_params() 79 | |> maybe_put_template_params(email) 80 | |> maybe_put_tag_params(email) 81 | |> maybe_put_attachments(email) 82 | end 83 | 84 | def maybe_put_attachments(params, %{attachments: []}) do 85 | params 86 | end 87 | 88 | def maybe_put_attachments(params, %{attachments: attachments}) do 89 | params 90 | |> Map.put(:"Attachments", Enum.map(attachments, fn attachment -> 91 | %{ 92 | Name: attachment.filename, 93 | Content: attachment.data |> Base.encode64(), 94 | ContentType: attachment.content_type, 95 | ContentId: attachment.content_id 96 | } 97 | end)) 98 | end 99 | 100 | defp maybe_put_template_params(params, %{private: 101 | %{template_id: template_name, template_model: template_model}}) do 102 | params 103 | |> Map.put(:"TemplateId", template_name) 104 | |> Map.put(:"TemplateModel", template_model) 105 | |> Map.put(:"InlineCss", true) 106 | end 107 | 108 | defp maybe_put_template_params(params, _) do 109 | params 110 | end 111 | 112 | defp maybe_put_tag_params(params, %{private: %{tag: tag}}) do 113 | Map.put(params, :"Tag", tag) 114 | end 115 | 116 | defp maybe_put_tag_params(params, _) do 117 | params 118 | end 119 | 120 | defp email_params(email) do 121 | recipients = recipients(email) 122 | add_message_params(%{ 123 | "From": email_from(email), 124 | "To": recipients_to_string(recipients, "To"), 125 | "Cc": recipients_to_string(recipients, "Cc"), 126 | "Bcc": recipients_to_string(recipients, "Bcc"), 127 | "Subject": email.subject, 128 | "TextBody": email.text_body, 129 | "HtmlBody": email.html_body, 130 | "Headers": email_headers(email), 131 | "TrackOpens": true 132 | }, email) 133 | end 134 | 135 | defp add_message_params(params, %{private: %{message_params: message_params}}) do 136 | Enum.reduce(message_params, params, fn({key, value}, params) -> 137 | Map.put(params, key, value) 138 | end) 139 | end 140 | defp add_message_params(params, _), do: params 141 | 142 | defp email_from(email) do 143 | name = elem(email.from, 0) 144 | email = elem(email.from, 1) 145 | if name do 146 | String.trim("#{name} <#{email}>") 147 | else 148 | String.trim(email) 149 | end 150 | end 151 | 152 | defp email_headers(email) do 153 | Enum.map(email.headers, 154 | fn {header, value} -> %{"Name": header, "Value": value} end) 155 | end 156 | 157 | defp recipients(email) do 158 | [] 159 | |> add_recipients(email.to, type: "To") 160 | |> add_recipients(email.cc, type: "Cc") 161 | |> add_recipients(email.bcc, type: "Bcc") 162 | end 163 | 164 | defp add_recipients(recipients, new_recipients, type: recipient_type) do 165 | Enum.reduce(new_recipients, recipients, fn(recipient, recipients) -> 166 | recipients ++ [%{ 167 | name: elem(recipient, 0), 168 | email: elem(recipient, 1), 169 | type: recipient_type 170 | }] 171 | end) 172 | end 173 | 174 | defp recipients_to_string(recipients, type) do 175 | recipients 176 | |> Enum.filter(fn(recipient) -> recipient[:type] == type end) 177 | |> Enum.map_join(",", fn(rec) -> "#{rec[:name]} <#{rec[:email]}>" end) 178 | end 179 | 180 | defp headers(api_key) do 181 | [{"accept", "application/json"}, 182 | {"content-type", "application/json"}, 183 | {"x-postmark-server-token", api_key}] 184 | end 185 | 186 | defp api_path(%{private: %{template_id: _}}), do: @send_email_template_path 187 | defp api_path(_), do: @send_email_path 188 | 189 | defp base_uri do 190 | Application.get_env(:bamboo, :postmark_base_uri) || @default_base_uri 191 | end 192 | 193 | defp options(config) do 194 | Keyword.merge(config[:request_options] || [], [with_body: true]) 195 | end 196 | end 197 | -------------------------------------------------------------------------------- /lib/bamboo/postmark_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule Bamboo.PostmarkHelper do 2 | @moduledoc """ 3 | Functions for using features specific to Postmark's templates. 4 | """ 5 | 6 | alias Bamboo.Email 7 | 8 | @doc """ 9 | Set a single tag for an email that allows you to categorize outgoing emails 10 | and get detailed statistics. 11 | 12 | A convenience function for `put_private(email, :tag, "my-tag")` 13 | 14 | ## Examples 15 | 16 | tag(email, "welcome-email") 17 | 18 | """ 19 | def tag(email, tag) do 20 | Email.put_private(email, :tag, tag) 21 | end 22 | 23 | @doc """ 24 | Send emails using Postmark's template API. 25 | 26 | Setup Postmark to send emails using a template. Use this in conjuction with 27 | the template content to offload template rendering to Postmark. The 28 | template id specified here must match the template id in Postmark. 29 | Postmarks's API docs for this can be found [here](https://postmarkapp.com/developer/api/templates-api#email-with-template). 30 | 31 | ## Examples 32 | 33 | template(email, "9746128") 34 | template(email, "9746128", %{"name" => "Name", "content" => "John"}) 35 | 36 | """ 37 | def template(email, template_id, template_model \\ %{}) do 38 | email 39 | |> Email.put_private(:template_id, template_id) 40 | |> Email.put_private(:template_model, template_model) 41 | end 42 | 43 | @doc """ 44 | Put extra message parameters that are used by Postmark. You can set things 45 | like TrackOpens, TrackLinks or Attachments. 46 | 47 | ## Examples 48 | 49 | put_param(email, "TrackLinks", "HtmlAndText") 50 | put_param(email, "TrackOpens", true) 51 | put_param(email, "Attachments", [ 52 | %{ 53 | Name: "file.txt", 54 | Content: "/some/file.txt" |> File.read!() |> Base.encode64(), 55 | ContentType: "txt" 56 | } 57 | ]) 58 | 59 | """ 60 | def put_param(%Email{private: %{message_params: _}} = email, key, value) do 61 | put_in(email.private[:message_params][key], value) 62 | end 63 | def put_param(email, key, value) do 64 | email 65 | |> Email.put_private(:message_params, %{}) 66 | |> put_param(key, value) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule BambooPostmark.Mixfile do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/pablo-co/bamboo_postmark" 5 | @version "1.0.0" 6 | 7 | def project do 8 | [ 9 | app: :bamboo_postmark, 10 | version: @version, 11 | elixir: "~> 1.4", 12 | name: "Bamboo Postmark", 13 | build_embedded: Mix.env() == :prod, 14 | start_permanent: Mix.env() == :prod, 15 | package: package(), 16 | deps: deps(), 17 | docs: docs() 18 | ] 19 | end 20 | 21 | def application do 22 | [ 23 | extra_applications: [:logger] 24 | ] 25 | end 26 | 27 | defp deps do 28 | [ 29 | {:bamboo, ">= 2.0.0"}, 30 | {:hackney, ">= 1.6.5"}, 31 | {:poison, ">= 1.5.0", only: :test}, 32 | {:plug, "~> 1.0"}, 33 | {:plug_cowboy, "~> 1.0", only: [:test, :dev]}, 34 | {:ex_doc, "> 0.0.0", only: :dev, runtime: false} 35 | ] 36 | end 37 | 38 | defp package do 39 | [ 40 | description: "A Bamboo adapter for Postmark", 41 | maintainers: ["Pablo Cárdenas"], 42 | licenses: ["MIT"], 43 | links: %{ 44 | "Changelog" => "https://hexdocs.pm/bamboo/postmark/changelog.html", 45 | "GitHub" => @source_url 46 | } 47 | ] 48 | end 49 | 50 | defp docs do 51 | [ 52 | extras: [ 53 | "CHANGELOG.md": [title: "Changelog"], 54 | "LICENSE.md": [title: "License"], 55 | "README.md": [title: "Overview"] 56 | ], 57 | main: "readme", 58 | source_url: @source_url, 59 | homepage_url: @source_url, 60 | formatters: ["html"] 61 | ] 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bamboo": {:hex, :bamboo, "2.0.2", "0e2914d2bea0de3b1743384c24ffbe20fbb58094376a49f1cf5d9ed9959abd82", [:mix], [{:hackney, ">= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.4", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "058d57cf4fcdac19413aa72732eb43c88954fb151a1cb6a382014e0cddbf6314"}, 3 | "certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"}, 4 | "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "f4763bbe08233eceed6f24bc4fcc8d71c17cfeafa6439157c57349aa1bb4f17c"}, 5 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm", "db622da03aa039e6366ab953e31186cc8190d32905e33788a1acb22744e6abd2"}, 6 | "earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm", "000aaeff08919e95e7aea13e4af7b2b9734577b3e6a7c50ee31ee88cab6ec4fb"}, 7 | "earmark_parser": {:hex, :earmark_parser, "1.4.13", "0c98163e7d04a15feb62000e1a891489feb29f3d10cb57d4f845c405852bbef8", [:mix], [], "hexpm", "d602c26af3a0af43d2f2645613f65841657ad6efc9f0e361c3b6c06b578214ba"}, 8 | "ex_doc": {:hex, :ex_doc, "0.24.2", "e4c26603830c1a2286dae45f4412a4d1980e1e89dc779fcd0181ed1d5a05c8d9", [:mix], [{:earmark_parser, "~> 1.4.0", [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", "e134e1d9e821b8d9e4244687fb2ace58d479b67b282de5158333b0d57c6fb7da"}, 9 | "hackney": {:hex, :hackney, "1.17.4", "99da4674592504d3fb0cfef0db84c3ba02b4508bae2dff8c0108baa0d6e0977c", [:rebar3], [{:certifi, "~>2.6.1", [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", "de16ff4996556c8548d512f4dbe22dd58a587bf3332e7fd362430a7ef3986b16"}, 10 | "httpoison": {:hex, :httpoison, "0.10.0", "4727b3a5e57e9a4ff168a3c2883e20f1208103a41bccc4754f15a9366f49b676", [:mix], [{:hackney, "~> 1.6.3", [hex: :hackney, optional: false]}]}, 11 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 12 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 13 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.1", "b5888c880d17d1cc3e598f05cdb5b5a91b7b17ac4eaf5f297cb697663a1094dd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "db68c173234b07ab2a07f645a5acdc117b9f99d69ebf521821d89690ae6c6ec8"}, 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, "1.5.0", "203ef35ef3389aae6d361918bf3f952fa17a09e8e43b5aa592b93eba05d0fb8d", [:mix], [], "hexpm", "55a94c0f552249fc1a3dd9cd2d3ab9de9d3c89b559c2bd01121f824834f24746"}, 17 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 18 | "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, 19 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 20 | "plug": {:hex, :plug, "1.11.1", "f2992bac66fdae679453c9e86134a4201f6f43a687d8ff1cd1b2862d53c80259", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "23524e4fefbb587c11f0833b3910bfb414bf2e2534d61928e920f54e3a1b881f"}, 21 | "plug_cowboy": {:hex, :plug_cowboy, "1.0.0", "2e2a7d3409746d335f451218b8bb0858301c3de6d668c3052716c909936eb57a", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "01d201427a8a1f4483be2465a98b45f5e82263327507fe93404a61c51eb9e9a8"}, 22 | "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, 23 | "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"}, 24 | "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm", "6e56493a862433fccc3aca3025c946d6720d8eedf6e3e6fb911952a7071c357f"}, 25 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 26 | "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, 27 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 28 | } 29 | -------------------------------------------------------------------------------- /test/lib/bamboo/postmark_adapter_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bamboo.PostmarkAdapterTest do 2 | use ExUnit.Case 3 | alias Bamboo.Email 4 | alias Bamboo.PostmarkAdapter 5 | alias Bamboo.PostmarkHelper 6 | 7 | @config %{adapter: PostmarkAdapter, api_key: "123_abc"} 8 | @config_with_env_var_key %{adapter: PostmarkAdapter, api_key: {:system, "POSTMARK_API_KEY"}} 9 | @config_with_bad_key %{adapter: PostmarkAdapter, api_key: nil} 10 | 11 | defmodule FakePostmark do 12 | use Plug.Router 13 | 14 | plug Plug.Parsers, 15 | parsers: [:urlencoded, :multipart, :json], 16 | pass: ["*/*"], 17 | json_decoder: Poison 18 | plug :match 19 | plug :dispatch 20 | 21 | def start_server(parent) do 22 | Agent.start_link(fn -> Map.new end, name: __MODULE__) 23 | Agent.update(__MODULE__, &Map.put(&1, :parent, parent)) 24 | port = get_free_port() 25 | Application.put_env(:bamboo, :postmark_base_uri, "http://localhost:#{port}") 26 | Plug.Adapters.Cowboy.http __MODULE__, [], port: port, ref: __MODULE__ 27 | end 28 | 29 | defp get_free_port do 30 | {:ok, socket} = :ranch_tcp.listen(port: 0) 31 | {:ok, port} = :inet.port(socket) 32 | :erlang.port_close(socket) 33 | port 34 | end 35 | 36 | def shutdown do 37 | Plug.Adapters.Cowboy.shutdown __MODULE__ 38 | end 39 | 40 | post "email" do 41 | case get_in(conn.params, ["From"]) do 42 | "INVALID_EMAIL" -> 43 | conn |> send_resp(500, "Error!!") |> send_to_parent 44 | _ -> 45 | conn |> send_resp(200, "SENT") |> send_to_parent 46 | end 47 | end 48 | 49 | post "email/withTemplate" do 50 | case get_in(conn.params, ["From"]) do 51 | "INVALID_EMAIL" -> 52 | conn |> send_resp(500, "Error!!") |> send_to_parent 53 | _ -> 54 | conn |> send_resp(200, "SENT") |> send_to_parent 55 | end 56 | end 57 | 58 | defp send_to_parent(conn) do 59 | parent = Agent.get(__MODULE__, fn(set) -> Map.get(set, :parent) end) 60 | send parent, {:fake_postmark, conn} 61 | conn 62 | end 63 | end 64 | 65 | setup do 66 | FakePostmark.start_server(self()) 67 | 68 | on_exit fn -> 69 | FakePostmark.shutdown 70 | end 71 | 72 | :ok 73 | end 74 | 75 | test "can read the api key from an ENV var" do 76 | System.put_env("POSTMARK_API_KEY", "123_abc") 77 | 78 | config = PostmarkAdapter.handle_config(@config_with_env_var_key) 79 | 80 | assert config[:api_key] == "123_abc" 81 | end 82 | 83 | test "raises if an invalid ENV var is used for the API key" do 84 | System.delete_env("POSTMARK_API_KEY") 85 | 86 | assert_raise ArgumentError, ~r/no API key set/, fn -> 87 | PostmarkAdapter.deliver(new_email(from: "foo@bar.com"), @config_with_env_var_key) 88 | end 89 | 90 | assert_raise ArgumentError, ~r/no API key set/, fn -> 91 | PostmarkAdapter.handle_config(@config_with_env_var_key) 92 | end 93 | end 94 | 95 | test "raises if the api key is nil" do 96 | assert_raise ArgumentError, ~r/no API key set/, fn -> 97 | PostmarkAdapter.deliver(new_email(from: "foo@bar.com"), @config_with_bad_key) 98 | end 99 | 100 | assert_raise ArgumentError, ~r/no API key set/, fn -> 101 | PostmarkAdapter.handle_config(%{}) 102 | end 103 | end 104 | 105 | test "deliver/2 passes the request_options to hackney" do 106 | request_options = [recv_timeout: 0] 107 | config = Map.put(@config, :request_options, request_options) 108 | 109 | 110 | {:error, %Bamboo.ApiError{message: message}} = PostmarkAdapter.deliver(new_email(), config) 111 | 112 | assert message.message =~ "timeout" 113 | end 114 | 115 | test "deliver/2 returns {:ok, response}, where response contains the textual body of the request" do 116 | {:ok, response} = PostmarkAdapter.deliver(new_email(), @config) 117 | 118 | assert response.body == "SENT" 119 | end 120 | 121 | test "deliver/2 makes the request to the right url" do 122 | PostmarkAdapter.deliver(new_email(), @config) 123 | 124 | assert_receive {:fake_postmark, %{request_path: request_path}} 125 | 126 | assert request_path == "/email" 127 | end 128 | 129 | test "deliver/2 sends the to the right url for templates" do 130 | new_email() |> PostmarkHelper.template("hello") |> PostmarkAdapter.deliver(@config) 131 | 132 | assert_receive {:fake_postmark, %{request_path: request_path}} 133 | 134 | assert request_path == "/email/withTemplate" 135 | end 136 | 137 | test "deliver/2 sends from, html and text body, subject, and headers" do 138 | email = 139 | [ 140 | from: {"From", "from@foo.com"}, 141 | subject: "My Subject", 142 | text_body: "TEXT BODY", 143 | html_body: "HTML BODY", 144 | ] 145 | |> new_email() 146 | |> Email.put_header("Reply-To", "reply@foo.com") 147 | 148 | PostmarkAdapter.deliver(email, @config) 149 | 150 | assert_receive {:fake_postmark, %{params: params}} 151 | assert params["From"] == "#{elem(email.from, 0)} <#{elem(email.from, 1)}>" 152 | assert params["Subject"] == email.subject 153 | assert params["TextBody"] == email.text_body 154 | assert params["HtmlBody"] == email.html_body 155 | assert params["Headers"] == 156 | [%{"Name" => "Reply-To", "Value" => "reply@foo.com"}] 157 | end 158 | 159 | test "deliver/2 correctly formats recipients" do 160 | email = new_email( 161 | to: [{"To", "to@bar.com"}], 162 | cc: [{"CC", "cc@bar.com"}], 163 | bcc: [{"BCC", "bcc@bar.com"}] 164 | ) 165 | 166 | PostmarkAdapter.deliver(email, @config) 167 | 168 | assert_receive {:fake_postmark, %{params: params}} 169 | assert params["To"] == "To " 170 | assert params["Bcc"] == "BCC " 171 | assert params["Cc"] == "CC " 172 | end 173 | 174 | test "deliver/2 puts template name and empty content" do 175 | email = PostmarkHelper.template(new_email(), "hello") 176 | 177 | PostmarkAdapter.deliver(email, @config) 178 | 179 | assert_receive {:fake_postmark, %{params: %{"TemplateId" => template_id, 180 | "TemplateModel" => template_model}}} 181 | assert template_id == "hello" 182 | assert template_model == %{} 183 | end 184 | 185 | test "deliver/2 puts template name and content" do 186 | email = PostmarkHelper.template(new_email(), "hello", [ 187 | %{name: "example name", content: "example content"} 188 | ]) 189 | 190 | PostmarkAdapter.deliver(email, @config) 191 | 192 | assert_receive {:fake_postmark, %{params: %{"TemplateId" => template_id, 193 | "TemplateModel" => template_model}}} 194 | assert template_id == "hello" 195 | assert template_model == [%{"content" => "example content", 196 | "name" => "example name"}] 197 | end 198 | 199 | test "deliver/2 puts tag param" do 200 | email = PostmarkHelper.tag(new_email(), "some_tag") 201 | 202 | PostmarkAdapter.deliver(email, @config) 203 | 204 | assert_receive {:fake_postmark, %{params: %{"Tag" => "some_tag"}}} 205 | end 206 | 207 | test "deliver/2 puts tracking params" do 208 | email = 209 | new_email() 210 | |> PostmarkHelper.template("hello") 211 | |> PostmarkHelper.put_param("TrackOpens", true) 212 | |> PostmarkHelper.put_param("TrackLinks", "HtmlOnly") 213 | 214 | PostmarkAdapter.deliver(email, @config) 215 | 216 | assert_receive {:fake_postmark, %{params: %{ 217 | "TrackLinks" => "HtmlOnly", "TrackOpens" => true, "TemplateId" => "hello"} 218 | }} 219 | end 220 | 221 | test "deliver/2 puts attachments" do 222 | email = 223 | new_email() 224 | |> Email.put_attachment(Path.join(__DIR__, "../../support/attachment.txt")) 225 | 226 | PostmarkAdapter.deliver(email, @config) 227 | 228 | assert_receive { 229 | :fake_postmark, 230 | %{ 231 | params: %{ 232 | "Attachments" => [ 233 | %{"Content" => "VGVzdCBBdHRhY2htZW50", "ContentType" => "text/plain", "Name" => "attachment.txt"} 234 | ] 235 | } 236 | } 237 | } 238 | end 239 | 240 | test "deliver/2 puts inline attachments" do 241 | email = PostmarkHelper.template(new_email(), "hello", [ 242 | %{name: "example name", content: "example content "} 243 | ]) 244 | |> Email.put_attachment(Path.join(__DIR__, "../../support/attachment.txt"), content_id: "my-attachment") 245 | 246 | PostmarkAdapter.deliver(email, @config) 247 | 248 | assert_receive { 249 | :fake_postmark, 250 | %{ 251 | params: %{ 252 | "Attachments" => [ 253 | %{ 254 | "Content" => "VGVzdCBBdHRhY2htZW50", 255 | "ContentType" => "text/plain", 256 | "Name" => "attachment.txt", 257 | "ContentId" => "my-attachment" 258 | } 259 | ] 260 | } 261 | } 262 | } 263 | end 264 | 265 | test "returns an error if the response is not a success" do 266 | email = new_email(from: "INVALID_EMAIL") 267 | 268 | {:error, %Bamboo.ApiError{message: message}} = PostmarkAdapter.deliver(email, @config) 269 | 270 | assert message.params =~ "INVALID_EMAIL" 271 | assert message.response == "Error!!" 272 | end 273 | 274 | defp new_email(attrs \\ []) do 275 | [from: "foo@bar.com", to: []] 276 | |> Keyword.merge(attrs) 277 | |> Email.new_email() 278 | |> Bamboo.Mailer.normalize_addresses() 279 | end 280 | end 281 | -------------------------------------------------------------------------------- /test/support/attachment.txt: -------------------------------------------------------------------------------- 1 | Test Attachment -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------