├── test ├── test_helper.exs ├── fixtures │ └── templates │ │ ├── email │ │ ├── welcome.text.eex │ │ ├── welcome.html.eex │ │ ├── welcome_assigns.text.eex │ │ ├── welcome.custom.eex │ │ ├── welcome_assigns.html.eex │ │ ├── format_html.htm.eex │ │ ├── format_html.xml.eex │ │ ├── format_text.txt.eex │ │ ├── welcome_assigns.custom.eex │ │ ├── format_html.html.eex │ │ ├── format_text.text.eex │ │ ├── format_text.unknown.eex │ │ ├── format_html.custom.eex │ │ └── welcome_from.html.eex │ │ ├── layout │ │ ├── email.text.eex │ │ ├── email.html.eex │ │ └── email.custom.eex │ │ ├── test_view_included_notifier │ │ ├── welcome_assigns.text.eex │ │ └── welcome_assigns.html.eex │ │ └── test_view_included_notifier_formats │ │ ├── welcome_assigns.text.eex │ │ └── welcome_assigns.custom.eex └── phoenix_swoosh_test.exs ├── .formatter.exs ├── .github ├── workflows │ ├── elixir.yml │ ├── publish-docs.yml │ ├── publish.yml │ └── release.yml ├── dependabot.yml └── release-drafter.yml ├── CONTRIBUTING.md ├── .gitignore ├── LICENSE.md ├── mix.exs ├── CHANGELOG.md ├── README.md ├── lib └── phoenix_swoosh.ex └── mix.lock /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/fixtures/templates/email/welcome.text.eex: -------------------------------------------------------------------------------- 1 | Welcome, Avengers! 2 | -------------------------------------------------------------------------------- /test/fixtures/templates/email/welcome.html.eex: -------------------------------------------------------------------------------- 1 |

Welcome, Avengers!

2 | -------------------------------------------------------------------------------- /test/fixtures/templates/layout/email.text.eex: -------------------------------------------------------------------------------- 1 | TEXT: <%= @inner_content %> 2 | -------------------------------------------------------------------------------- /test/fixtures/templates/email/welcome_assigns.text.eex: -------------------------------------------------------------------------------- 1 | Welcome, <%= @name %>! 2 | -------------------------------------------------------------------------------- /test/fixtures/templates/layout/email.html.eex: -------------------------------------------------------------------------------- 1 | <%= @inner_content %> 2 | -------------------------------------------------------------------------------- /test/fixtures/templates/email/welcome.custom.eex: -------------------------------------------------------------------------------- 1 | Welcome, Avengers! 2 | -------------------------------------------------------------------------------- /test/fixtures/templates/email/welcome_assigns.html.eex: -------------------------------------------------------------------------------- 1 |

Welcome, <%= @name %>!

2 | -------------------------------------------------------------------------------- /test/fixtures/templates/layout/email.custom.eex: -------------------------------------------------------------------------------- 1 | <%= @inner_content %> 2 | -------------------------------------------------------------------------------- /test/fixtures/templates/email/format_html.htm.eex: -------------------------------------------------------------------------------- 1 | This is an HTML template with the .htm extension. 2 | -------------------------------------------------------------------------------- /test/fixtures/templates/email/format_html.xml.eex: -------------------------------------------------------------------------------- 1 | This is an HTML template with the .xml extension. 2 | -------------------------------------------------------------------------------- /test/fixtures/templates/email/format_text.txt.eex: -------------------------------------------------------------------------------- 1 | This is a text template with the .txt extension. 2 | -------------------------------------------------------------------------------- /test/fixtures/templates/email/welcome_assigns.custom.eex: -------------------------------------------------------------------------------- 1 | Welcome, <%= @name %>! 2 | -------------------------------------------------------------------------------- /test/fixtures/templates/test_view_included_notifier/welcome_assigns.text.eex: -------------------------------------------------------------------------------- 1 | Welcome, <%= @name %>! 2 | -------------------------------------------------------------------------------- /test/fixtures/templates/email/format_html.html.eex: -------------------------------------------------------------------------------- 1 | This is an HTML template with the .html extension. 2 | -------------------------------------------------------------------------------- /test/fixtures/templates/email/format_text.text.eex: -------------------------------------------------------------------------------- 1 | This is a text template with the .text extension. 2 | -------------------------------------------------------------------------------- /test/fixtures/templates/email/format_text.unknown.eex: -------------------------------------------------------------------------------- 1 | This is a text template with an unknown extension. 2 | -------------------------------------------------------------------------------- /test/fixtures/templates/email/format_html.custom.eex: -------------------------------------------------------------------------------- 1 | This is an HTML template with the .custom extension. 2 | -------------------------------------------------------------------------------- /test/fixtures/templates/test_view_included_notifier/welcome_assigns.html.eex: -------------------------------------------------------------------------------- 1 |

Welcome, <%= @name %>!

2 | -------------------------------------------------------------------------------- /test/fixtures/templates/test_view_included_notifier_formats/welcome_assigns.text.eex: -------------------------------------------------------------------------------- 1 | Welcome, <%= @name %>! 2 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /test/fixtures/templates/email/welcome_from.html.eex: -------------------------------------------------------------------------------- 1 | <% {_name, address} = @email.from %>
Welcome from <%= address %>
2 | -------------------------------------------------------------------------------- /test/fixtures/templates/test_view_included_notifier_formats/welcome_assigns.custom.eex: -------------------------------------------------------------------------------- 1 | Welcome, <%= @name %>! 2 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | uses: swoosh/actions/.github/workflows/test.yml@main 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: mix 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: '10:00' 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/publish-docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docs 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | publish: 7 | uses: swoosh/actions/.github/workflows/publish.yml@main 8 | with: 9 | mode: 'docs' 10 | secrets: 11 | HEX_API_KEY: ${{ secrets.HEX_API_KEY }} 12 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | publish: 10 | uses: swoosh/actions/.github/workflows/publish.yml@main 11 | with: 12 | mode: 'package' 13 | secrets: 14 | HEX_API_KEY: ${{ secrets.HEX_API_KEY }} 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - main 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: release-drafter/release-drafter@v5 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Running tests 4 | 5 | Clone the repo and fetch its dependencies: 6 | 7 | ```bash 8 | $ git clone https://github.com/swoosh/phoenix_swoosh.git 9 | $ cd phoenix_swoosh 10 | $ mix deps.get 11 | $ mix test 12 | ``` 13 | 14 | ## Building docs 15 | 16 | Documentation is written into the library, you will find it in the source code, 17 | accessible from `iex` and of course, it all gets published to 18 | [hexdocs](http://hexdocs.pm/phoenix_swoosh). 19 | 20 | ```bash 21 | $ mix docs 22 | ``` 23 | -------------------------------------------------------------------------------- /.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 | phoenix_swoosh-*.tar 24 | 25 | # Temporary files for e.g. tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION 🚀' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | categories: 4 | - title: '✨ Features' 5 | labels: 6 | - 'feature' 7 | - 'enhancement' 8 | - title: '🐛 Bug Fixes' 9 | labels: 10 | - 'fix' 11 | - 'bugfix' 12 | - 'bug' 13 | - title: '🧰 Maintenance' 14 | label: 15 | - 'chore' 16 | - 'dependencies' 17 | - title: '📝 Documentation' 18 | label: 'docs' 19 | exclude-labels: 20 | - 'skip-changelog' 21 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 22 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 23 | version-resolver: 24 | major: 25 | labels: 26 | - 'major' 27 | minor: 28 | labels: 29 | - 'minor' 30 | patch: 31 | labels: 32 | - 'patch' 33 | default: patch 34 | template: | 35 | ## Changes 36 | 37 | $CHANGES 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Swoosh contributors 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 all 13 | 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 THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixSwoosh.Mixfile do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/swoosh/phoenix_swoosh" 5 | @version "1.2.1" 6 | 7 | def project do 8 | [ 9 | app: :phoenix_swoosh, 10 | version: @version, 11 | elixir: "~> 1.11", 12 | name: "Phoenix.Swoosh", 13 | build_embedded: Mix.env() == :prod, 14 | start_permanent: Mix.env() == :prod, 15 | deps: deps(), 16 | package: package(), 17 | docs: docs(), 18 | preferred_cli_env: [docs: :docs] 19 | ] 20 | end 21 | 22 | def application do 23 | [extra_applications: [:logger]] 24 | end 25 | 26 | defp deps do 27 | [ 28 | {:swoosh, "~> 1.5"}, 29 | {:phoenix_view, "~> 1.0 or ~> 2.0"}, 30 | {:phoenix_html, "~> 3.0 or ~> 4.0", optional: true}, 31 | {:hackney, "~> 1.10", optional: true}, 32 | {:finch, "~> 0.8", optional: true}, 33 | {:phoenix, "~> 1.8", optional: true}, 34 | {:credo, "~> 1.0", only: [:dev, :test]}, 35 | {:ex_doc, "~> 0.26", only: :docs, runtime: false} 36 | ] 37 | end 38 | 39 | defp package do 40 | [ 41 | description: "Use Swoosh to easily send emails in your Phoenix project.", 42 | maintainers: ["Steve Domin", "Po Chen"], 43 | licenses: ["MIT"], 44 | links: %{ 45 | "Changelog" => "https://hexdocs.pm/phoenix_swoosh/changelog.html", 46 | "GitHub" => @source_url 47 | } 48 | ] 49 | end 50 | 51 | defp docs do 52 | [ 53 | extras: [ 54 | {:"README.md", [title: "Overview"]}, 55 | "CHANGELOG.md", 56 | "CONTRIBUTING.md", 57 | "LICENSE.md": [title: "License"] 58 | ], 59 | main: "readme", 60 | canonical: "http://hexdocs.pm/phoenix_swoosh", 61 | source_url: @source_url, 62 | source_ref: "v#{@version}", 63 | api_reference: false, 64 | formatters: ["html"] 65 | ] 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /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 | ## 1.2.1 - 2024-01-08 9 | 10 | ### Maintenance 11 | 12 | - Relax `phoenix_html` dependency requirement, allowing `~> 4.0` 13 | 14 | ## 1.2.0 - 2023-02-19 15 | 16 | ### Feature 17 | 18 | - Support extension customization @jswanner (#290) 19 | 20 | See customization section in readme for more details. 21 | 22 | ## 1.1.0 - 2022-10-27 23 | 24 | ### Changes 25 | 26 | - Support `phoenix_view` 2.0 @dkln (#279) 27 | 28 | ## 1.0.1 - 2022-02-12 29 | 30 | ### Changes 31 | 32 | - Remove transitive compile-time deps in Phoenix.Swoosh macro @MamesPalmero (#241) 33 | 34 | ## 1.0.0 - 2021-09-25 35 | 36 | ### Added 37 | 38 | The setup within a `Phoenix` is now referred to as the 39 | [classic setup](https://github.com/swoosh/phoenix_swoosh#1-classic-setup). 40 | 41 | 1.0 adds the ability for the lib to be used outside `Phoenix` apps. 42 | 43 | A new setup that doesn't involve a sparate view module is added and is called the standalone setup. 44 | Both setups can work outsite `Phoenix` apps thanks to the recently extracted `Phoenix.View`. 45 | 46 | #### Standalone setup 47 | 48 | ```eex 49 | # path_to/templates/user_notifier/welcome.html.eex 50 |
51 |

Welcome to Sample, <%= @name %>!

52 |
53 | ``` 54 | 55 | ```elixir 56 | # path_to/notifiers/user_notifier.ex 57 | defmodule Sample.UserNotifier do 58 | use Phoenix.Swoosh, 59 | template_root: "path_to/templates", 60 | template_path: "user_notifier" 61 | 62 | # ... same welcome ... 63 | end 64 | ``` 65 | 66 | In this setup, the notifier module itself serves as the view module 67 | 68 | `template_root`, `template_path` and `template_namespace` 69 | will be passed to `Phoenix.View` as `root`, `path` and `namespace`. 70 | 71 | Layout can be setup the same way as 72 | [classic setup](https://github.com/swoosh/phoenix_swoosh#1-classic-setup). 73 | 74 | --- 75 | 76 | [Changelog prior to 1.0 can be found on 0.3.x branch](https://github.com/swoosh/phoenix_swoosh/blob/0.3.x/CHANGELOG.md) 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Phoenix.Swoosh 2 | 3 | [![Elixir CI](https://github.com/swoosh/phoenix_swoosh/actions/workflows/elixir.yml/badge.svg)](https://github.com/swoosh/phoenix_swoosh/actions/workflows/elixir.yml) 4 | [![Module Version](https://img.shields.io/hexpm/v/phoenix_swoosh.svg)](https://hex.pm/packages/phoenix_swoosh) 5 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/phoenix_swoosh/) 6 | [![Total Download](https://img.shields.io/hexpm/dt/phoenix_swoosh.svg)](https://hex.pm/packages/phoenix_swoosh) 7 | [![License](https://img.shields.io/hexpm/l/phoenix_swoosh.svg)](https://github.com/swoosh/phoenix_swoosh/blob/master/LICENSE) 8 | [![Last Updated](https://img.shields.io/github/last-commit/swoosh/phoenix_swoosh.svg)](https://github.com/swoosh/phoenix_swoosh/commits/master) 9 | 10 | `Phoenix.View` + `Swoosh`. ([Discuss about the future post-Phoenix-1.7 here](https://github.com/swoosh/phoenix_swoosh/issues/287)) 11 | 12 | This module provides the ability to set the HTML and/or text body of an email by rendering templates. 13 | 14 | ## Installation 15 | 16 | Add `:phoenix_swoosh` to your list of dependencies in `mix.exs`: 17 | 18 | ```elixir 19 | def deps do 20 | [ 21 | {:phoenix_swoosh, "~> 1.0"}, 22 | 23 | # without phoenix_html, phoenix_swoosh only works with plain text templates 24 | # if you want to use HTML templates 25 | # {:phoenix_html, "~> 3.0"}, 26 | ] 27 | end 28 | ``` 29 | 30 | You probably also want to install [`finch`](https://hex.pm/packages/finch), 31 | [`hackney`](https://hex.pm/packages/hackney) or an HTTP client of your own choice, 32 | if you are using a provider that `Swoosh` talks to via their HTTP API. 33 | 34 | Or [`:gen_smtp`](https://hex.pm/packages/gen_smtp) if you are working with a provider 35 | that only works through SMTP. 36 | 37 | See `Swoosh` for more details. 38 | 39 | ## Usage 40 | 41 | ### 1. Classic setup 42 | 43 | Setting up the templates: 44 | 45 | ```eex 46 | # path_to/templates/user_notifier/welcome.html.eex 47 |
48 |

Welcome to Sample, <%= @name %>!

49 |
50 | ``` 51 | 52 | ```elixir 53 | # path_to/views/user_notifier_view.ex 54 | defmodule Sample.UserNotifierView do 55 | use Phoenix.View, root: "path_to/templates" 56 | end 57 | ``` 58 | 59 | Passing values to templates: 60 | 61 | ```elixir 62 | # path_to/notifiers/user_notifier.ex 63 | defmodule Sample.UserNotifier do 64 | use Phoenix.Swoosh, view: Sample.UserNotifierView 65 | 66 | def welcome(user) do 67 | new() 68 | |> from("tony@stark.com") 69 | |> to(user.email) 70 | |> subject("Hello, Avengers!") 71 | |> render_body("welcome.html", %{name: name}) 72 | end 73 | end 74 | ``` 75 | 76 | Maybe with a layout: 77 | 78 | ```eex 79 | # path_to/templates/layout/email.html.eex 80 | 81 | 82 | <%= @email.subject %> 83 | 84 | 85 | <%= @inner_content %> 86 | 87 | 88 | ``` 89 | 90 | ```elixir 91 | defmodule Sample.LayoutView do 92 | use Phoenix.View, root: "path_to/templates" 93 | end 94 | ``` 95 | 96 | ```elixir 97 | # path_to/notifiers/user_notifier.ex 98 | defmodule Sample.UserNotifier do 99 | use Phoenix.Swoosh, 100 | view: Sample.NotifierView, 101 | layout: {Sample.LayoutView, :email} 102 | 103 | # ... same welcome ... 104 | end 105 | ``` 106 | 107 | Layout can also be added/changed dynamically with `put_new_layout/2` and `put_layout/2` 108 | 109 | ### 2. Standalone setup 110 | 111 | ```eex 112 | # path_to/templates/user_notifier/welcome.html.eex 113 |
114 |

Welcome to Sample, <%= @name %>!

115 |
116 | ``` 117 | 118 | ```elixir 119 | # path_to/notifiers/user_notifier.ex 120 | defmodule Sample.UserNotifier do 121 | use Phoenix.Swoosh, 122 | template_root: "path_to/templates", 123 | template_path: "user_notifier" 124 | 125 | # ... same welcome ... 126 | end 127 | ``` 128 | 129 | In this setup, the notifier module itself serves as the view module 130 | 131 | `template_root`, `template_path` and `template_namespace` 132 | will be passed to `Phoenix.View` as `root`, `path` and `namespace`. 133 | 134 | Layout can be setup the same way as classic setup. 135 | 136 | ## Customization 137 | 138 | When using either the classic or standalone setup described above, the 139 | extensions of the templates used can be customized. For example, let's say 140 | MJML is the desired markup language to use for HTML emails, and there's a 141 | template such as: 142 | 143 | ```eex 144 | # path_to/templates/user_notifier/welcome.mjml.eex 145 | 146 | 147 | 148 | Welcome to Sample, <%= @name %>! 149 | 150 | 151 | ``` 152 | 153 | Phoenix.Swoosh can be configured to use the "mjml" extension when rendering the 154 | HTML body of the email: 155 | 156 | ```elixir 157 | # path_to/notifiers/user_notifier.ex 158 | defmodule Sample.UserNotifier do 159 | use Phoenix.Swoosh, 160 | formats: %{"mjml" => :html_body, "text" => :text_body} 161 | 162 | # ... same welcome as above ... 163 | end 164 | ``` 165 | 166 | This requires that Phoenix knows how to handle templates using the "mjml" 167 | format. This can be done by creating a module that exports 168 | `encode_to_iodata!/1`: 169 | 170 | ```elixir 171 | defmodule Sample.Mjml do 172 | def encode_to_iodata!(mjml) do 173 | with {:ok, html} <- Mjml.to_html(mjml) do 174 | html 175 | end 176 | end 177 | end 178 | ``` 179 | 180 | And then configuring Phoenix to use it: 181 | 182 | ```elixir 183 | config :phoenix, format_encoders: [mjml: Sample.Mjml] 184 | ``` 185 | 186 | The above example is using [`mjml`](https://hex.pm/packages/mjml), which would 187 | need to be installed as well. 188 | 189 | ## Copyright and License 190 | 191 | Copyright (c) 2021 Swoosh contributors 192 | 193 | Released under the MIT License, which can be found in [LICENSE.md](./LICENSE.md). 194 | -------------------------------------------------------------------------------- /lib/phoenix_swoosh.ex: -------------------------------------------------------------------------------- 1 | defmodule Phoenix.Swoosh do 2 | @moduledoc """ 3 | The main feature provided by this module is the ability to set the HTML and/or 4 | text body of an email by rendering templates. 5 | 6 | It utilizes `Phoenix.View` and can work very well both standalone 7 | and in apps using `Phoenix` framework. 8 | 9 | 10 | `use Phoenix.Swoosh` also accepts a list of options which configures where 11 | templates will be looked up: 12 | 13 | * `:view` - for "classic" setup, the view module to use for rendering 14 | * `:template_path` - for "standalone" setup, given to `Phoenix.View` as `path` 15 | * `:template_root` - for "standalone" setup, given to `Phoenix.View` as `root` 16 | * `:template_namespace` - for "standalone" setup, given to `Phoenix.View` as 17 | `namespace` 18 | * `:layout` - the layout to render the templates in. Must be a tuple, 19 | specifying the layout view and the layout name, or `false` 20 | * `:formats` - to customize the extensions of the templates. Must be a map, 21 | with key being the extension and the value being the body field to set 22 | """ 23 | 24 | import Swoosh.Email 25 | 26 | defmacro __using__(opts) do 27 | view = Keyword.get(opts, :view) 28 | layout = Keyword.get(opts, :layout, false) 29 | template_root = Keyword.get(opts, :template_root) 30 | template_path = Keyword.get(opts, :template_path) 31 | template_namespace = Keyword.get(opts, :template_namespace) 32 | formats = Keyword.get(opts, :formats) 33 | 34 | unless view || template_root do 35 | raise ArgumentError, 36 | """ 37 | no view or template_root was set, you can set one with 38 | 39 | use Phoenix.Swoosh, view: MyApp.EmailView 40 | 41 | or 42 | 43 | use Phoenix.Swoosh, template_root: "./templates" 44 | """ 45 | end 46 | 47 | view_module = if template_root, do: quote(do: __MODULE__), else: view 48 | 49 | quote do 50 | import Swoosh.Email 51 | import Phoenix.Swoosh, except: [render_body: 3] 52 | 53 | if unquote(template_root) do 54 | use Phoenix.View, 55 | root: unquote(template_root), 56 | path: unquote(template_path), 57 | namespace: unquote(template_namespace) 58 | end 59 | 60 | def render_body(email, template, assigns \\ %{}) do 61 | email 62 | |> put_new_formats(unquote(formats)) 63 | |> put_new_layout(unquote(layout)) 64 | |> put_new_view(unquote(view_module)) 65 | |> Phoenix.Swoosh.render_body(template, assigns) 66 | end 67 | end 68 | end 69 | 70 | @doc """ 71 | Renders the given `template` and `assigns` based on the `email`. 72 | 73 | Once the template is rendered the resulting string is stored on the email 74 | fields `html_body` and `text_body` depending on the format of the template. 75 | `.html`, `.htm`, and `.xml` are stored in `html_body`; all other extensions, 76 | (e.g. `.txt` and `.text`), in `text_body`. 77 | 78 | ## Arguments 79 | 80 | * `email` - the `Swoosh.Email` struct. 81 | 82 | * `template` - may be an atom or a string. If an atom, like `:welcome`, it 83 | will render both the HTML and text template and stores them respectively on 84 | the email. If the template is a string it must contain the extension too, 85 | like `welcome.html`. 86 | 87 | * `assigns` - a dictionary with the assigns to be used in the view. Those 88 | assigns are merged and have higher order precedence than the email assigns. 89 | (`email.assigns`) 90 | 91 | ## Examples 92 | 93 | defmodule Sample.UserEmail do 94 | use Phoenix.Swoosh, view: Sample.EmailView 95 | 96 | def welcome(user) do 97 | new() 98 | |> from("tony@stark.com") 99 | |> to(user.email) 100 | |> subject("Hello, Avengers!") 101 | |> render_body("welcome.html", %{username: user.email}) 102 | end 103 | end 104 | 105 | The example above renders a template `welcome.html` from `Sample.EmailView` and 106 | stores the resulting string onto the html_body field of the email. 107 | (`email.html_body`) 108 | 109 | In many cases you may want to set both the html and text body of an email. To 110 | do so you can pass the template name as an atom (without the extension): 111 | 112 | def welcome(user) do 113 | new() 114 | |> from("tony@stark.com") 115 | |> to(user.email) 116 | |> subject("Hello, Avengers!") 117 | |> render_body(:welcome, %{username: user.email}) 118 | end 119 | 120 | ## Layouts 121 | 122 | Templates are often rendered inside layouts. If you wish to do so you will have 123 | to specify which layout you want to use when using the `Phoenix.Swoosh` module. 124 | 125 | defmodule Sample.UserEmail do 126 | use Phoenix.Swoosh, view: Sample.EmailView, layout: {Sample.LayoutView, :email} 127 | 128 | def welcome(user) do 129 | new() 130 | |> from("tony@stark.com") 131 | |> to(user.email) 132 | |> subject("Hello, Avengers!") 133 | |> render_body("welcome.html", %{username: user.email}) 134 | end 135 | end 136 | 137 | The example above will render the `welcome.html` template inside an 138 | `email.html` template specified in `Sample.LayoutView`. `put_layout/2` can be 139 | used to change the layout, similar to how `put_view/2` can be used to change 140 | the view. 141 | """ 142 | def render_body(email, template, assigns) when is_atom(template) do 143 | extensions(email) 144 | |> Enum.reduce(email, fn extension, email -> 145 | do_render_body(email, template_name(template, extension), extension, assigns) 146 | end) 147 | end 148 | 149 | def render_body(email, template, assigns) when is_binary(template) do 150 | case Path.extname(template) do 151 | "." <> extension -> 152 | do_render_body(email, template, extension, assigns) 153 | 154 | "" -> 155 | raise "cannot render template #{inspect(template)} without format. Use an atom if you " <> 156 | "want to set both the html and text body." 157 | end 158 | end 159 | 160 | defp do_render_body(email, template, extension, assigns) do 161 | assigns = Enum.into(assigns, %{}) 162 | 163 | email = 164 | email 165 | |> put_private(:phoenix_template, template) 166 | |> prepare_assigns(assigns, extension) 167 | 168 | view = 169 | Map.get(email.private, :phoenix_view) || 170 | raise "a view module was not specified, set one with put_view/2" 171 | 172 | content = Phoenix.View.render_to_string(view, template, Map.put(email.assigns, :email, email)) 173 | Map.put(email, extension_to_body_key(email, extension), content) 174 | end 175 | 176 | @doc """ 177 | Stores the formats for rendering if none was stored yet. 178 | """ 179 | def put_new_formats(email, nil), do: email 180 | 181 | def put_new_formats(email, extension_format_map) do 182 | update_in(email.private, fn private -> 183 | private 184 | |> Map.put_new(:phoenix_extensions, Map.keys(extension_format_map)) 185 | |> Map.put_new(:phoenix_extensions_to_body_key, extension_format_map) 186 | end) 187 | end 188 | 189 | @doc """ 190 | Stores the layout for rendering. 191 | 192 | The layout must be a tuple, specifying the layout view and the layout 193 | name, or false. In case a previous layout is set, `put_layout` also 194 | accepts the layout name to be given as a string or as an atom. If a 195 | string, it must contain the format. Passing an atom means the layout 196 | format will be found at rendering time, similar to the template in 197 | `render_body/3`. It can also be set to `false`. In this case, no 198 | layout would be used. 199 | 200 | ## Examples 201 | 202 | iex> layout(email) 203 | false 204 | 205 | iex> email = put_layout email, {LayoutView, "email.html"} 206 | iex> layout(email) 207 | {LayoutView, "email.html"} 208 | 209 | iex> email = put_layout email, "email.html" 210 | iex> layout(email) 211 | {LayoutView, "email.html"} 212 | 213 | iex> email = put_layout email, :email 214 | iex> layout(email) 215 | {AppView, :email} 216 | 217 | """ 218 | def put_layout(email, layout) do 219 | do_put_layout(email, layout) 220 | end 221 | 222 | defp do_put_layout(email, false) do 223 | put_private(email, :phoenix_layout, false) 224 | end 225 | 226 | defp do_put_layout(email, {mod, layout}) when is_atom(mod) do 227 | put_private(email, :phoenix_layout, {mod, layout}) 228 | end 229 | 230 | defp do_put_layout(email, layout) when is_binary(layout) or is_atom(layout) do 231 | update_in(email.private, fn private -> 232 | case Map.get(private, :phoenix_layout, false) do 233 | {mod, _} -> 234 | Map.put(private, :phoenix_layout, {mod, layout}) 235 | 236 | false -> 237 | raise "cannot use put_layout/2 with atom/binary when layout is false, use a tuple instead" 238 | end 239 | end) 240 | end 241 | 242 | @doc """ 243 | Stores the layout for rendering if one was not stored yet. 244 | """ 245 | def put_new_layout(email, layout) 246 | when (is_tuple(layout) and tuple_size(layout) == 2) or layout == false do 247 | update_in(email.private, &Map.put_new(&1, :phoenix_layout, layout)) 248 | end 249 | 250 | @doc """ 251 | Retrieves the current layout of an email. 252 | """ 253 | def layout(email), do: Map.get(email.private, :phoenix_layout, false) 254 | 255 | @doc """ 256 | Stores the view for rendering. 257 | """ 258 | def put_view(email, module) do 259 | put_private(email, :phoenix_view, module) 260 | end 261 | 262 | @doc """ 263 | Stores the view for rendering if one was not stored yet. 264 | """ 265 | def put_new_view(email, module) do 266 | update_in(email.private, &Map.put_new(&1, :phoenix_view, module)) 267 | end 268 | 269 | defp prepare_assigns(email, assigns, extension) do 270 | layout = 271 | case layout(email, assigns, extension) do 272 | {mod, layout} -> {mod, template_name(layout, extension)} 273 | false -> false 274 | end 275 | 276 | update_in( 277 | email.assigns, 278 | &(&1 |> Map.merge(assigns) |> Map.put(:layout, layout)) 279 | ) 280 | end 281 | 282 | @default_mapping %{ 283 | "htm" => :html_body, 284 | "html" => :html_body, 285 | "text" => :text_body, 286 | "xml" => :html_body 287 | } 288 | 289 | defp extension_to_body_key(email, extension) do 290 | email.private 291 | |> Map.get(:phoenix_extensions_to_body_key, @default_mapping) 292 | |> Map.get(extension, :text_body) 293 | end 294 | 295 | defp extensions(email) do 296 | Map.get(email.private, :phoenix_extensions, ["html", "text"]) 297 | end 298 | 299 | defp layout(email, assigns, extension) do 300 | if extension in extensions(email) do 301 | case Map.fetch(assigns, :layout) do 302 | {:ok, layout} -> layout 303 | :error -> layout(email) 304 | end 305 | else 306 | false 307 | end 308 | end 309 | 310 | defp template_name(name, extension) when is_atom(name), 311 | do: Atom.to_string(name) <> "." <> extension 312 | 313 | defp template_name(name, _ext) when is_binary(name), do: name 314 | end 315 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 | "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, 4 | "credo": {:hex, :credo, "1.7.14", "c7e75216cea8d978ba8c60ed9dede4cc79a1c99a266c34b3600dd2c33b96bc92", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "12a97d6bb98c277e4fb1dff45aaf5c137287416009d214fb46e68147bd9e0203"}, 5 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 6 | "ex_doc": {:hex, :ex_doc, "0.39.3", "519c6bc7e84a2918b737aec7ef48b96aa4698342927d080437f61395d361dcee", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "0590955cf7ad3b625780ee1c1ea627c28a78948c6c0a9b0322bd976a079996e1"}, 7 | "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, 8 | "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"}, 9 | "hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.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.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"}, 10 | "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, 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 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 13 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 14 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 15 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 16 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 17 | "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, 18 | "mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"}, 19 | "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, 20 | "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, 21 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 22 | "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, 23 | "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, 24 | "phoenix": {:hex, :phoenix, "1.8.3", "49ac5e485083cb1495a905e47eb554277bdd9c65ccb4fc5100306b350151aa95", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "36169f95cc2e155b78be93d9590acc3f462f1e5438db06e6248613f27c80caec"}, 25 | "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, 26 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, 27 | "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, 28 | "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, 29 | "plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"}, 30 | "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, 31 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, 32 | "swoosh": {:hex, :swoosh, "1.19.9", "4eb2c471b8cf06adbdcaa1d57a0ad53c0ed9348ce8586a06cc491f9f0dbcb553", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "516898263a64925c31723c56bc7999a26e97b04e869707f681f4c9bca7ee1688"}, 33 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, 34 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, 35 | "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, 36 | "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"}, 37 | } 38 | -------------------------------------------------------------------------------- /test/phoenix_swoosh_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Phoenix.SwooshTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Swoosh.Email 5 | import Swoosh.Email 6 | import Phoenix.Swoosh 7 | 8 | defmodule EmailView do 9 | use Phoenix.View, root: "test/fixtures/templates", namespace: Phoenix.SwooshTest 10 | end 11 | 12 | defmodule LayoutView do 13 | use Phoenix.View, root: "test/fixtures/templates", namespace: Phoenix.SwooshTest 14 | end 15 | 16 | defmodule TestEmail do 17 | use Phoenix.Swoosh, view: EmailView 18 | 19 | def welcome_html(), do: email() |> render_body("welcome.html", %{}) 20 | def welcome_text(), do: email() |> render_body("welcome.text", %{}) 21 | 22 | def welcome_html_assigns(), 23 | do: email() |> render_body("welcome_assigns.html", %{name: "Tony"}) 24 | 25 | def welcome_text_assigns(), 26 | do: email() |> render_body("welcome_assigns.text", %{name: "Tony"}) 27 | 28 | def welcome_html_without_assigns(), do: email() |> render_body("welcome.html") 29 | def welcome_text_without_assigns(), do: email() |> render_body("welcome.text") 30 | 31 | def welcome_html_layout() do 32 | email() 33 | |> put_layout({LayoutView, "email.html"}) 34 | |> render_body("welcome.html", %{}) 35 | end 36 | 37 | def welcome_text_layout() do 38 | email() 39 | |> put_layout({LayoutView, "email.text"}) 40 | |> render_body("welcome.text", %{}) 41 | end 42 | 43 | def welcome_html_layout_without_assigns() do 44 | email() 45 | |> put_layout({LayoutView, "email.html"}) 46 | |> render_body("welcome.html") 47 | end 48 | 49 | def welcome_text_layout_without_assigns() do 50 | email() 51 | |> put_layout({LayoutView, "email.text"}) 52 | |> render_body("welcome.text") 53 | end 54 | 55 | def welcome_html_layout_assigns() do 56 | email() 57 | |> put_layout({LayoutView, "email.html"}) 58 | |> render_body("welcome_assigns.html", %{name: "Tony"}) 59 | end 60 | 61 | def welcome_text_layout_assigns() do 62 | email() 63 | |> put_layout({LayoutView, "email.text"}) 64 | |> render_body("welcome_assigns.text", %{name: "Tony"}) 65 | end 66 | 67 | def welcome(), do: email() |> render_body(:welcome, %{}) 68 | 69 | def welcome_assigns(), do: email() |> render_body(:welcome_assigns, %{name: "Tony"}) 70 | 71 | def welcome_layout() do 72 | email() 73 | |> put_layout({LayoutView, :email}) 74 | |> render_body(:welcome, %{}) 75 | end 76 | 77 | def welcome_layout_assigns() do 78 | email() 79 | |> put_layout({LayoutView, :email}) 80 | |> render_body(:welcome_assigns, %{name: "Tony"}) 81 | end 82 | 83 | def email() do 84 | %Email{} 85 | |> from("tony@stark.com") 86 | |> to("steve@rogers.com") 87 | |> subject("Welcome, Avengers!") 88 | end 89 | end 90 | 91 | defmodule TestEmailLayout do 92 | use Phoenix.Swoosh, view: EmailView, layout: {LayoutView, :email} 93 | 94 | def welcome() do 95 | %Email{} 96 | |> from("tony@stark.com") 97 | |> to("steve@rogers.com") 98 | |> subject("Welcome, Avengers!") 99 | |> render_body(:welcome, %{}) 100 | end 101 | end 102 | 103 | defmodule TestEmailLayoutFormats do 104 | use Phoenix.Swoosh, 105 | formats: %{"custom" => :html_body, "text" => :text_body}, 106 | view: EmailView, 107 | layout: {LayoutView, :email} 108 | 109 | def welcome do 110 | %Email{} 111 | |> from("tony@stark.com") 112 | |> to("steve@rogers.com") 113 | |> subject("Welcome, Avengers!") 114 | |> render_body(:welcome, %{}) 115 | end 116 | end 117 | 118 | defmodule TestViewIncludedNotifier do 119 | use Phoenix.Swoosh, 120 | template_root: "test/fixtures/templates", 121 | template_namespace: Phoenix.SwooshTest 122 | 123 | import Swoosh.Email 124 | 125 | def welcome_assigns do 126 | %Email{} 127 | |> from("tony@stark.com") 128 | |> to("steve@rogers.com") 129 | |> subject("Welcome, Avengers!") 130 | |> render_body(:welcome_assigns, %{name: "Tony"}) 131 | end 132 | 133 | def welcome_layout do 134 | %Email{} 135 | |> from("tony@stark.com") 136 | |> to("steve@rogers.com") 137 | |> subject("Welcome, Avengers!") 138 | |> put_layout({LayoutView, :email}) 139 | |> render_body(:welcome_assigns, %{name: "Avengers"}) 140 | end 141 | end 142 | 143 | defmodule TestViewIncludedNotifierFormats do 144 | use Phoenix.Swoosh, 145 | formats: %{"custom" => :html_body, "text" => :text_body}, 146 | template_root: "test/fixtures/templates", 147 | template_namespace: Phoenix.SwooshTest 148 | 149 | import Swoosh.Email 150 | 151 | def welcome_assigns do 152 | %Email{} 153 | |> from("tony@stark.com") 154 | |> to("steve@rogers.com") 155 | |> subject("Welcome, Avengers!") 156 | |> render_body(:welcome_assigns, %{name: "Tony"}) 157 | end 158 | 159 | def welcome_layout do 160 | %Email{} 161 | |> from("tony@stark.com") 162 | |> to("steve@rogers.com") 163 | |> subject("Welcome, Avengers!") 164 | |> put_layout({LayoutView, :email}) 165 | |> render_body(:welcome_assigns, %{name: "Avengers"}) 166 | end 167 | end 168 | 169 | setup_all do 170 | email = 171 | %Email{} 172 | |> from("tony@stark.com") 173 | |> to("steve@rogers.com") 174 | |> subject("Welcome, Avengers!") 175 | |> put_view(EmailView) 176 | 177 | {:ok, email: email} 178 | end 179 | 180 | test "render html body", %{email: email} do 181 | assert %Email{html_body: "

Welcome, Avengers!

\n"} = 182 | render_body(email, "welcome.html", %{}) 183 | end 184 | 185 | test "render html body with layout", %{email: email} do 186 | email = email |> put_layout({LayoutView, "email.html"}) 187 | 188 | assert %Email{html_body: "

Welcome, Avengers!

\n\n"} = 189 | render_body(email, "welcome.html", %{}) 190 | end 191 | 192 | test "render text body", %{email: email} do 193 | assert %Email{text_body: "Welcome, Avengers!\n"} = render_body(email, "welcome.text", %{}) 194 | end 195 | 196 | test "render text body with layout", %{email: email} do 197 | email = email |> put_layout({LayoutView, "email.text"}) 198 | 199 | assert %Email{text_body: "TEXT: Welcome, Avengers!\n\n"} = 200 | render_body(email, "welcome.text", %{}) 201 | end 202 | 203 | test "render html body with assigns", %{email: email} do 204 | assert %Email{html_body: "

Welcome, Tony!

\n"} = 205 | render_body(email, "welcome_assigns.html", %{name: "Tony"}) 206 | end 207 | 208 | test "render text body with assigns", %{email: email} do 209 | assert %Email{text_body: "Welcome, Tony!\n"} = 210 | render_body(email, "welcome_assigns.text", %{name: "Tony"}) 211 | end 212 | 213 | test "render html body with layout and assigns", %{email: email} do 214 | email = email |> put_layout({LayoutView, "email.html"}) 215 | 216 | assert %Email{html_body: "

Welcome, Tony!

\n\n"} = 217 | render_body(email, "welcome_assigns.html", %{name: "Tony"}) 218 | end 219 | 220 | test "render text body with layout and assigns", %{email: email} do 221 | email = email |> put_layout({LayoutView, "email.text"}) 222 | 223 | assert %Email{text_body: "TEXT: Welcome, Tony!\n\n"} = 224 | render_body(email, "welcome_assigns.text", %{name: "Tony"}) 225 | end 226 | 227 | test "render both html and text body", %{email: email} do 228 | assert %Email{html_body: "

Welcome, Avengers!

\n", text_body: "Welcome, Avengers!\n"} = 229 | render_body(email, :welcome, %{}) 230 | end 231 | 232 | test "render both html and text body with layout", %{email: email} do 233 | email = email |> put_layout({LayoutView, :email}) 234 | 235 | assert %Email{ 236 | html_body: "

Welcome, Avengers!

\n\n", 237 | text_body: "TEXT: Welcome, Avengers!\n\n" 238 | } = render_body(email, :welcome, %{}) 239 | end 240 | 241 | test "render both html and text body with assigns", %{email: email} do 242 | assert %Email{html_body: "

Welcome, Tony!

\n", text_body: "Welcome, Tony!\n"} = 243 | render_body(email, :welcome_assigns, %{name: "Tony"}) 244 | end 245 | 246 | test "render both html and text body with layout and assigns", %{email: email} do 247 | email = email |> put_layout({LayoutView, :email}) 248 | 249 | assert %Email{ 250 | html_body: "

Welcome, Tony!

\n\n", 251 | text_body: "TEXT: Welcome, Tony!\n\n" 252 | } = render_body(email, :welcome_assigns, %{name: "Tony"}) 253 | end 254 | 255 | test "macro: render html body" do 256 | assert %Email{html_body: "

Welcome, Avengers!

\n"} = TestEmail.welcome_html() 257 | end 258 | 259 | test "macro: render text body" do 260 | assert %Email{text_body: "Welcome, Avengers!\n"} = TestEmail.welcome_text() 261 | end 262 | 263 | test "macro: render html body with layout" do 264 | assert %Email{html_body: "

Welcome, Avengers!

\n\n"} = 265 | TestEmail.welcome_html_layout() 266 | end 267 | 268 | test "macro: render text body with layout" do 269 | assert %Email{text_body: "TEXT: Welcome, Avengers!\n\n"} = TestEmail.welcome_text_layout() 270 | end 271 | 272 | test "macro: render html body with layout without assigns" do 273 | assert %Email{html_body: "

Welcome, Avengers!

\n\n"} = 274 | TestEmail.welcome_html_layout_without_assigns() 275 | end 276 | 277 | test "macro: render text body with layout without assigns" do 278 | assert %Email{text_body: "TEXT: Welcome, Avengers!\n\n"} = 279 | TestEmail.welcome_text_layout_without_assigns() 280 | end 281 | 282 | test "macro: render html body without assigns" do 283 | assert %Email{html_body: "

Welcome, Avengers!

\n"} = 284 | TestEmail.welcome_html_without_assigns() 285 | end 286 | 287 | test "macro: render text body without assigns" do 288 | assert %Email{text_body: "Welcome, Avengers!\n"} = TestEmail.welcome_text_without_assigns() 289 | end 290 | 291 | test "macro: render html body with assigns" do 292 | assert %Email{html_body: "

Welcome, Tony!

\n"} = TestEmail.welcome_html_assigns() 293 | end 294 | 295 | test "macro: render text body with assigns" do 296 | assert %Email{text_body: "Welcome, Tony!\n"} = TestEmail.welcome_text_assigns() 297 | end 298 | 299 | test "macro: render html body with layout and assigns" do 300 | assert %Email{html_body: "

Welcome, Tony!

\n\n"} = 301 | TestEmail.welcome_html_layout_assigns() 302 | end 303 | 304 | test "macro: render text body with layout and assigns" do 305 | assert %Email{text_body: "TEXT: Welcome, Tony!\n\n"} = TestEmail.welcome_text_layout_assigns() 306 | end 307 | 308 | test "macro: render both html and text body" do 309 | assert %Email{html_body: "

Welcome, Avengers!

\n", text_body: "Welcome, Avengers!\n"} = 310 | TestEmail.welcome() 311 | end 312 | 313 | test "macro: render both html and text body with layout" do 314 | assert %Email{ 315 | html_body: "

Welcome, Avengers!

\n\n", 316 | text_body: "TEXT: Welcome, Avengers!\n\n" 317 | } = TestEmail.welcome_layout() 318 | end 319 | 320 | test "macro: render both html and text body with assigns" do 321 | assert %Email{html_body: "

Welcome, Tony!

\n", text_body: "Welcome, Tony!\n"} = 322 | TestEmail.welcome_assigns() 323 | end 324 | 325 | test "macro: render both html and text body with layout and assigns" do 326 | assert %Email{ 327 | html_body: "

Welcome, Tony!

\n\n", 328 | text_body: "TEXT: Welcome, Tony!\n\n" 329 | } = TestEmail.welcome_layout_assigns() 330 | end 331 | 332 | test "macro: use layout when provided via `use` macro" do 333 | assert %Email{ 334 | html_body: "

Welcome, Avengers!

\n\n", 335 | text_body: "TEXT: Welcome, Avengers!\n\n" 336 | } = TestEmailLayout.welcome() 337 | end 338 | 339 | test "macro: use layout for custom format when provided via `use` macro" do 340 | assert %Email{ 341 | html_body: "Welcome, Avengers!\n\n", 342 | text_body: "TEXT: Welcome, Avengers!\n\n" 343 | } = TestEmailLayoutFormats.welcome() 344 | end 345 | 346 | test "email is available in template", %{email: email} do 347 | assert %Email{html_body: "
Welcome from tony@stark.com
\n"} = 348 | render_body(email, "welcome_from.html", %{}) 349 | end 350 | 351 | test "put_layout/2", %{email: email} do 352 | email = 353 | email 354 | |> put_layout({LayoutView, :wrong}) 355 | |> put_layout(:email) 356 | |> render_body("welcome.html", %{}) 357 | 358 | assert %Email{html_body: "

Welcome, Avengers!

\n\n"} = email 359 | end 360 | 361 | describe "view included" do 362 | test "render both html and text body with assigns" do 363 | assert %Email{html_body: "

Welcome, Tony!

\n", text_body: "Welcome, Tony!\n"} = 364 | TestViewIncludedNotifier.welcome_assigns() 365 | end 366 | 367 | test "render both html and text body with layout" do 368 | assert %Email{ 369 | html_body: "

Welcome, Avengers!

\n\n", 370 | text_body: "TEXT: Welcome, Avengers!\n\n" 371 | } = TestViewIncludedNotifier.welcome_layout() 372 | end 373 | end 374 | 375 | describe "view included and formats defined" do 376 | test "render both html and text body with assigns" do 377 | assert %Email{ 378 | html_body: "Welcome, Tony!\n", 379 | text_body: "Welcome, Tony!\n" 380 | } = TestViewIncludedNotifierFormats.welcome_assigns() 381 | end 382 | 383 | test "render both html and text body with layout" do 384 | assert %Email{ 385 | html_body: "Welcome, Avengers!\n\n", 386 | text_body: "TEXT: Welcome, Avengers!\n\n" 387 | } = TestViewIncludedNotifierFormats.welcome_layout() 388 | end 389 | end 390 | 391 | test "should raise if no view is set" do 392 | assert_raise ArgumentError, fn -> 393 | defmodule ErrorEmail do 394 | use Phoenix.Swoosh 395 | end 396 | end 397 | end 398 | 399 | test "body formats are set according to template file extension", %{email: email} do 400 | assert email |> render_body("format_html.html", %{}) |> Map.fetch!(:html_body) =~ 401 | "HTML template with the .html extension" 402 | 403 | assert email |> render_body("format_html.htm", %{}) |> Map.fetch!(:html_body) =~ 404 | "HTML template with the .htm extension" 405 | 406 | assert email |> render_body("format_html.xml", %{}) |> Map.fetch!(:html_body) =~ 407 | "HTML template with the .xml extension" 408 | 409 | assert email |> render_body("format_text.txt", %{}) |> Map.fetch!(:text_body) =~ 410 | "text template with the .txt extension" 411 | 412 | assert email |> render_body("format_text.text", %{}) |> Map.fetch!(:text_body) =~ 413 | "text template with the .text extension" 414 | 415 | assert email |> render_body("format_text.unknown", %{}) |> Map.fetch!(:text_body) =~ 416 | "text template with an unknown extension" 417 | end 418 | end 419 | --------------------------------------------------------------------------------