Skip to main content

lib/sendgrid/email.ex

defmodule SendGrid.Email do
  @moduledoc """
  Email primitive for composing emails with SendGrid's API.

  You can easily compose on an Email to set the fields of your email.

  ## Example

      Email.build()
      |> Email.add_to("test@email.com")
      |> Email.put_from("test2@email.com")
      |> Email.put_subject("Hello from Elixir")
      |> Email.put_text("Sent with Elixir")
      |> SendGrid.Mail.send()

  ## SendGrid Specific Features

  Many common features of SendGrid V3 API for transactional emails are supported.

  ### Templates

  You can use a SendGrid template by providing a template id.

      put_template(email, "some_template_id")

  ### Substitutions

  You can provided a key-value pair for subsititions to have text replaced.

      add_substitution(email, "-key-", "value")

  ### Scheduled Sending

  You can provide a Unix timestamp to have an email delivered in the future.

      put_send_at(email, 1409348513)

  ## Phoenix Views

  You can use Phoenix Views to set your HTML and text content of your emails. You just have
  to provide a view module and template name and you're good to go! Additionally, you can set
  a layout to render the view in with `put_phoenix_layout/2`. See `put_phoenix_template/3`
  for complete usage.

  ### Examples

      # Using an HTML template
      %Email{}
      |> put_phoenix_view(MyApp.Web.EmailView)
      |> put_phoenix_template("welcome_email.html", user: user)

      # Using a text template
      %Email{}
      |> put_phoenix_view(MyApp.Web.EmailView)
      |> put_phoenix_template("welcome_email.txt", user: user)

      # Using both an HTML and text template
      %Email{}
      |> put_phoenix_view(MyApp.Web.EmailView)
      |> put_phoenix_template(:welcome_email, user: user)

      # Setting the layout
      %Email{}
      |> put_phoenix_layout({MyApp.Web.EmailView, :layout})
      |> put_phoenix_view(MyApp.Web.EmailView)
      |> put_phoenix_template(:welcome_email, user: user)


  ### Using a Default Phoenix View

  You can set a default Phoenix View to use for rendering templates. Just set the `:phoenix_view`
  config value.

      config :noizu_sendgrid,
        phoenix_view: MyApp.Web.EmailView


  ### Using a Default View Layout

  You can set a default layout to render the view in. Just set the `:phoenix_layout` config value.

      config :noizu_sendgrid,
        phoenix_layout: {MyApp.Web.EmailView, :layout}

  """

  alias SendGrid.{Email, Personalization}

  defstruct to: nil,
            cc: nil,
            bcc: nil,
            from: nil,
            reply_to: nil,
            reply_to_list: nil,
            subject: nil,
            content: nil,
            template_id: nil,
            version_id: nil,
            substitutions: nil,
            custom_args: nil,
            personalizations: nil,
            send_at: nil,
            headers: nil,
            categories: nil,
            batch_id: nil,
            asm: nil,
            ip_pool_name: nil,
            mail_settings: nil,
            tracking_settings: nil,
            attachments: nil,
            dynamic_template_data: nil,
            sandbox: false,
            __phoenix_view__: nil,
            __phoenix_layout__: nil

  @type t :: %Email{
          to: nil | [recipient],
          cc: nil | [recipient],
          bcc: nil | [recipient],
          from: nil | recipient,
          reply_to: nil | recipient,
          reply_to_list: nil | [recipient],
          subject: nil | String.t(),
          content: nil | [content],
          template_id: nil | String.t(),
          version_id: nil | String.t(),
          substitutions: nil | substitutions,
          custom_args: nil | custom_args,
          personalizations: nil | [Personalization.t()],
          dynamic_template_data: nil | dynamic_template_data,
          send_at: nil | integer,
          headers: nil | headers(),
          categories: nil | [String.t],
          batch_id: nil | String.t,
          asm: nil |  asm(),
          ip_pool_name: nil | String.t,
          mail_settings: nil | mail_settings(),
          tracking_settings: nil | tracking_settings(),
          attachments: nil | [attachment],
          sandbox: boolean(),
          __phoenix_view__: nil | atom,
          __phoenix_layout__:
            nil | %{optional(:text) => String.t(), optional(:html) => String.t()}
        }

  @type recipient :: %{required(:email) => String.t(), optional(:name) => String.t()}
  @type content :: %{type: String.t(), value: String.t()}
  @type headers :: %{String.t() => String.t()}
  @type asm :: %{
                 required(:group_id) => integer,
                 optional(:groups_to_display) => [integer]
               }
  @type mail_settings :: %{
                           optional(:bypass_list_management) => bypass_filter(),
                           optional(:bypass_spam_management) => bypass_filter(),
                           optional(:bypass_bounce_management) => bypass_filter(),
                           optional(:bypass_unsubscribe_management) => bypass_filter(),
                           optional(:footer) => footer(),
                           optional(:sandbox_mode) => enable_status()
                         }
  @type attachment :: %{
          required(:content) => String.t(),
          optional(:type) => String.t(),
          required(:filename) => String.t(),
          optional(:disposition) => String.t(),
          optional(:content_id) => String.t()
        }

  @type enable_status ::%{
    required(:enable) => boolean
  }
  @type bypass_filter :: enable_status()
  @type footer :: %{
                    required(:enable) => boolean,
                    required(:text) => String.t,
                    required(:html) => String.t
                  }
  @type substitutions :: %{String.t() => String.t()}
  @type custom_args :: %{String.t() => String.t()}
  @type dynamic_template_data :: %{String.t() => String.t()}

  @type tracking_settings :: %{
                            optional(:click_tracking) => click_tracking(),
                            optional(:open_tracking) => open_tracking(),
                            optional(:subscription_tracking) => subscription_tracking(),
                            optional(:ganalytics) => google_analytics_tracking()
                          }

  @type click_tracking :: %{
                            optional(:enable) => boolean,
                            optional(:enable_text) => boolean
                          }
  @type open_tracking :: %{
                            optional(:enable) => boolean,
                            optional(:substitution_tag) => nil | String.t
                          }
  @type subscription_tracking :: %{
                                   optional(:enable) => boolean,
                                   optional(:text) => String.t,
                                   optional(:html) => String.t,
                                   optional(:substitution_tag) => String.t
                                 }
  @type google_analytics_tracking :: %{
                        optional(:enable) => boolean,
                        optional(:utm_source) => String.t,
                        optional(:utm_medium) => String.t,
                        optional(:utm_content) => String.t,
                        optional(:utm_campaign) => String.t
                      }


  @doc """
  Builds an an empty email to compose on.

  ## Examples

      iex> build()
      %Email{...}

  """
  @spec build :: t
  def build do
    %Email{}
  end

  @doc """
  Sets the `to` field for the email. A to-name can be passed as the third parameter.

  ## Examples

      add_to(%Email{}, "test@email.com")
      add_to(%Email{}, "test@email.com", "John Doe")

  """
  @spec add_to(t, String.t()) :: t
  def add_to(%Email{to: to} = email, to_address) do
    addresses = add_address_to_list(to, to_address)
    %Email{email | to: addresses}
  end

  @spec add_to(t, String.t(), String.t()) :: t
  def add_to(%Email{to: to} = email, to_address, to_name) do
    addresses = add_address_to_list(to, to_address, to_name)
    %Email{email | to: addresses}
  end

  @doc """
  Sets the `from` field for the email. The from-name can be specified as the third parameter.

  ## Examples

      put_from(%Email{}, "test@email.com")
      put_from(%Email{}, "test@email.com", "John Doe")

  """
  @spec put_from(t, String.t()) :: t
  def put_from(%Email{} = email, from_address) do
    %Email{email | from: address(from_address)}
  end

  @spec put_from(t, String.t(), String.t()) :: t
  def put_from(%Email{} = email, from_address, from_name) do
    %Email{email | from: address(from_address, from_name)}
  end

  @doc """
  Add recipients to the `CC` address field. The cc-name can be specified as the third parameter.

  ## Examples

      add_cc(%Email{}, "test@email.com")
      add_cc(%Email{}, "test@email.com", "John Doe")

  """
  @spec add_cc(t, String.t()) :: t
  def add_cc(%Email{cc: cc} = email, cc_address) do
    addresses = add_address_to_list(cc, cc_address)
    %Email{email | cc: addresses}
  end

  @spec add_cc(Email.t(), String.t(), String.t()) :: Email.t()
  def add_cc(%Email{cc: cc} = email, cc_address, cc_name) do
    addresses = add_address_to_list(cc, cc_address, cc_name)
    %Email{email | cc: addresses}
  end

  @doc """
  Add recipients to the `BCC` address field. The bcc-name can be specified as the third parameter.

  ## Examples

      add_bcc(%Email{}, "test@email.com")
      add_bcc(%Email{}, "test@email.com", "John Doe")

  """
  @spec add_bcc(t, String.t()) :: t
  def add_bcc(%Email{bcc: bcc} = email, bcc_address) do
    addresses = add_address_to_list(bcc, bcc_address)
    %Email{email | bcc: addresses}
  end

  @spec add_bcc(t, String.t(), String.t()) :: t
  def add_bcc(%Email{bcc: bcc} = email, bcc_address, bcc_name) do
    addresses = add_address_to_list(bcc, bcc_address, bcc_name)
    %Email{email | bcc: addresses}
  end

  @doc """
  Adds an attachment to the email.

  An attachment is a map with the keys:

    * `:content`
    * `:type`
    * `:filename`
    * `:disposition`
    * `:content_id`

  ## Examples

      attachment = %{content: "base64string", filename: "image.jpg"}
      add_attachment(%Email{}, attachment}

  """
  @spec add_attachment(t, attachment) :: t
  def add_attachment(%Email{} = email, attachment) do
    attachments =
      case email.attachments do
        nil -> [attachment]
        list -> list ++ [attachment]
      end

    %Email{email | attachments: attachments}
  end

  @doc """
  Sets the `reply_to` field for the email. The reply-to name can be specified as the third parameter.

  ## Examples

      put_reply_to(%Email{}, "test@email.com")
      put_reply_to(%Email{}, "test@email.com", "John Doe")

  """
  @spec put_reply_to(t, String.t()) :: t
  def put_reply_to(%Email{} = email, reply_to_address) do
    %Email{email | reply_to: address(reply_to_address)}
  end

  @spec put_reply_to(t, String.t(), String.t()) :: t
  def put_reply_to(%Email{} = email, reply_to_address, reply_to_name) do
    %Email{email | reply_to: address(reply_to_address, reply_to_name)}
  end



  @doc """
  Sets the `reply_to_list` field for email. You may not use reply_to_list and reply_to at the same time.
  """
  @spec put_reply_to_list(t, [String.t()]) :: t
  def put_reply_to_list(%Email{} = email, reply_to_addresses) do
    list = Enum.map(reply_to_addresses, fn(v) ->
      case v do
        {address, name} -> address(address, name)
        address -> address(address)
      end
    end)
    %Email{email | reply_to_list: list}
  end

  @doc """
  Set/Replace email categories.
  """
  def put_categories(%Email{} = email, categories) do
    %Email{email| categories: categories}
  end
  
  @doc """
  Add/set email category.
  """
  def put_category(%Email{} = email, category) do
    case email.categories do
      nil -> %Email{email| categories: [category]}
      v -> %Email{email| categories: Enum.uniq(v ++ [category])}
    end
  end

  @doc """
  Set batch_id
  """
  def put_batch(%Email{} = email, value) do
    %Email{email| batch_id: value}
  end

  @doc """
  Set asm
  """
  def put_asm(%Email{} = email, value) do
    %Email{email| asm: value}
  end


  @doc """
  Set ip_pool_name
  """
  def put_ip_pool(%Email{} = email, value) do
    %Email{email| ip_pool_name: value}
  end

  # Initialize mail_settings
  defp init_mail_settings(%Email{mail_settings: nil} = email) do
    %Email{email| mail_settings: %{}}
  end
  defp init_mail_settings(%Email{} = email), do: email

  @doc """
  Set email.mail_settings.bypass_list_management
  """
  def configure_list_management_bypass(%Email{} = email, enable) do
    init_mail_settings(email)
    |> put_in([Access.key(:mail_settings), :bypass_list_management], %{enable: enable})
  end

  @doc """
  Set email.mail_settings.bypass_spam_management
  """
  def configure_spam_management_bypass(%Email{} = email, enable) do
    init_mail_settings(email)
    |> put_in([Access.key(:mail_settings), :bypass_spam_management], %{enable: enable})
  end

  @doc """
  Set email.mail_settings.bypass_bounce_management
  """
  def configure_bounce_management_bypass(%Email{} = email, enable) do
    init_mail_settings(email)
    |> put_in([Access.key(:mail_settings), :bypass_bounce_management], %{enable: enable})
  end

  @doc """
  Set email.mail_settings.bypass_unsubscribe_management
  """
  def configure_unsubscribe_management_bypass(%Email{} = email, enable) do
    init_mail_settings(email)
    |> put_in([Access.key(:mail_settings), :bypass_unsubscribe_management], %{enable: enable})
  end
  
  @doc """
  Set email.mail_settings.bypass_unsubscribe_management
  """
  def put_footer(%Email{} = email, footer) do
    init_mail_settings(email)
    |> put_in([Access.key(:mail_settings), :footer], footer)
  end

  @doc """
  Set entire mail_settings field, replacing any previous settings.
  """
  def put_mail_settings(%Email{} = email, value) do
    %Email{mail_settings: value}
  end


  # Initialize track_settings
  defp init_tracking_settings(%Email{tracking_settings: nil} = email) do
    %Email{email| tracking_settings: %{}}
  end
  defp init_tracking_settings(%Email{} = email), do: email

  def configure_click_tracking(%Email{} = email, value) do
    email
    |> init_tracking_settings()
    |> put_in([Access.key(:tracking_settings), :click_tracking], value)
  end
  
  def configure_open_tracking(%Email{} = email, value) do
    email
    |> init_tracking_settings()
    |> put_in([Access.key(:tracking_settings), :open_tracking], value)
  end
  
  def configure_subscription_tracking(%Email{} = email, value) do
    email
    |> init_tracking_settings()
    |> put_in([Access.key(:tracking_settings), :subscription_tracking], value)
  end

  def configure_google_analytics(%Email{} = email, value) do
    email
    |> init_tracking_settings()
    |> put_in([Access.key(:tracking_settings), :ganalytics], value)
  end

  def put_tracking_settings(%Email{} = email, value) do
    email
    |> put_in([Access.key(:tracking_settings)], value)
  end



  @doc """
  Sets the `subject` field for the email.

  ## Examples

      put_subject(%Email{}, "Hello from Elixir")

  """
  @spec put_subject(t, String.t()) :: t
  def put_subject(%Email{} = email, subject) do
    %Email{email | subject: subject}
  end

  @doc """
  Sets `text` content of the email.

  ## Examples

      put_text(%Email{}, "Sent from Elixir!")

  """
  @spec put_text(t, String.t()) :: t
  def put_text(%Email{content: [%{type: "text/plain"} | tail]} = email, text_body) do
    content = [%{type: "text/plain", value: text_body} | tail]
    %Email{email | content: content}
  end

  def put_text(%Email{content: content} = email, text_body) do
    content = [%{type: "text/plain", value: text_body} | List.wrap(content)]
    %Email{email | content: content}
  end

  @doc """
  Sets the `html` content of the email.

  ## Examples

      Email.put_html(%Email{}, "<html><body><p>Sent from Elixir!</p></body></html>")

  """
  @spec put_html(t, String.t()) :: t
  def put_html(%Email{content: [head | %{type: "text/html"}]} = email, html_body) do
    content = [head | %{type: "text/html", value: html_body}]
    %Email{email | content: content}
  end

  def put_html(%Email{content: content} = email, html_body) do
    content = List.wrap(content) ++ [%{type: "text/html", value: html_body}]
    %Email{email | content: content}
  end

  @doc """
  Sets a custom header.

  ## Examples

      Email.add_header(%Email{}, "HEADER_KEY", "HEADER_VALUE")

  """
  @spec add_header(t, String.t(), String.t()) :: t
  def add_header(%Email{headers: headers} = email, header_key, header_value)
      when is_binary(header_key) and is_binary(header_value) do
    new_headers = Map.put(headers || %{}, header_key, header_value)
    %Email{email | headers: new_headers}
  end

  @doc """
  Uses a predefined SendGrid template for the email.

  ## Examples

      Email.put_template(%Email{}, "the_template_id")

  """
  @spec put_template(t, String.t() | SendGrid.Template) :: t
  def put_template(%Email{} = email, template = %SendGrid.LegacyTemplate{}), do: put_template(email, template.id)
  def put_template(%Email{} = email, template = %SendGrid.DynamicTemplate{}), do: put_template(email, template.id)
  def put_template(%Email{} = email, template_id) do
    %Email{email | template_id: template_id}
  end
  
  @doc """
  Uses a predefined SendGrid template version for the email.

  ## Examples

      Email.put_template_version(%Email{}, "the_template_version_id")

  """
  @spec put_template_version(t, String.t()) :: t
  def put_template_version(%Email{} = email, version_id) when is_bitstring(version_id) do
    %Email{email | version_id: version_id}
  end
  
  
  
  @doc """
  Adds a substitution value to be used with a template.

  If a substitution for a given name is already set, it will be replaced when adding
  a substitution with the same name.

  ## Examples

      Email.add_substitution(%Email{}, "-sentIn-", "Elixir")

  """
  @spec add_substitution(t, String.t(), String.t()) :: t
  def add_substitution(%Email{substitutions: substitutions} = email, sub_name, sub_value) do
    substitutions = Map.put(substitutions || %{}, sub_name, sub_value)
    %Email{email | substitutions: substitutions}
  end

  @doc """
  Adds a custom_arg value to the email.

  If an argument for a given name is already set, it will be replaced when adding
  a argument with the same name.

  ## Examples

      Email.add_custom_arg(%Email{}, "-sentIn-", "Elixir")

  """
  @spec add_custom_arg(t, String.t(), String.t()) :: t
  def add_custom_arg(%Email{custom_args: custom_args} = email, arg_name, arg_value) do
    custom_args = Map.put(custom_args || %{}, arg_name, arg_value)
    %Email{email | custom_args: custom_args}
  end

  @doc """
  Adds a custom_arg value to the email.

  If an argument for a given name is already set, it will be replaced when adding
  a argument with the same name.

  ## Examples

      Email.add_dynamic_template_data(%Email{}, "-sentIn-", "Elixir")

  """
  @spec add_dynamic_template_data(t, String.t(), String.t()) :: t
  def add_dynamic_template_data(
        %Email{dynamic_template_data: dynamic_template_data} = email,
        arg_name,
        arg_value
      ) do
    dynamic_template_data = Map.put(dynamic_template_data || %{}, arg_name, arg_value)
    %Email{email | dynamic_template_data: dynamic_template_data}
  end

  @doc """
  Sets a future date of when to send the email.

  ## Examples

      Email.put_send_at(%Email{}, 1409348513)

  """
  @spec put_send_at(t, integer) :: t
  def put_send_at(%Email{} = email, send_at) do
    %Email{email | send_at: send_at}
  end

  defp address(email), do: %{email: email}
  defp address(email, name), do: %{email: email, name: name}

  defp add_address_to_list(nil, email) do
    [address(email)]
  end

  defp add_address_to_list(list, email) when is_list(list) do
    list ++ [address(email)]
  end

  defp add_address_to_list(nil, email, name) do
    [address(email, name)]
  end

  defp add_address_to_list(list, email, name) when is_list(list) do
    list ++ [address(email, name)]
  end

  @doc """
  Sets the layout to use for the Phoenix Template.

  Expects a tuple of the view module and layout to use. If you provide an atom as the second element,
  the text and HMTL versions of that template will be used for the respective content types.

  Alernatively, you can set a default layout to use by setting the `:phoenix_view` key in your config as
  an atom which will be used for both text and HTML emails.

      config :noizu_sendgrid,
        phoenix_layout: {MyApp.Web.EmailView, :layout}

  ## Examples

      put_phoenix_layout(email, {MyApp.Web.EmailView, "layout.html"})
      put_phoenix_layout(email, {MyApp.Web.EmailView, "layout.txt"})
      put_phoenix_layout(email, {MyApp.Web.EmailView, :layout})

  """
  @spec put_phoenix_layout(t, {atom, atom}) :: t
  def put_phoenix_layout(%Email{} = email, {module, layout})
      when is_atom(module) and is_atom(layout) do
    layouts = build_layouts({module, layout})
    %Email{email | __phoenix_layout__: layouts}
  end

  @spec put_phoenix_layout(t, {atom, String.t()}) :: t
  def put_phoenix_layout(%Email{__phoenix_layout__: layouts} = email, {module, layout})
      when is_atom(module) do
    layouts = layouts || %{}
    updated_layout = build_layouts({module, layout})
    %Email{email | __phoenix_layout__: Map.merge(layouts, updated_layout)}
  end

  # Build layout map
  defp build_layouts({module, layout}) when is_atom(module) and is_atom(layout) do
    base_name = Atom.to_string(layout)

    %{
      text: {module, base_name <> ".txt"},
      html: {module, base_name <> ".html"}
    }
  end

  defp build_layouts({module, layout} = args) when is_atom(module) do
    case Path.extname(layout) do
      ".html" -> %{html: args}
      ".txt" -> %{text: args}
      _ -> raise ArgumentError, "unsupported file type"
    end
  end

  @doc """
  Sets the Phoenix View to use.

  This will override the default Phoenix View if set in under the `:phoenix_view`
  config value.

  ## Examples

      put_phoenix_view(email, MyApp.Web.EmailView)

  """
  @spec put_phoenix_view(t, atom) :: t
  def put_phoenix_view(%Email{} = email, module) when is_atom(module) do
    %Email{email | __phoenix_view__: module}
  end

  @doc """
  Renders the Phoenix template with the given assigns.

  You can set the default Phoenix View to use for your templates by setting the `:phoenix_view` config value.
  Additionally, you can set the view on a per email basis by calling `put_phoenix_view/2`. Furthermore, you can have
  the template rendered inside a layout. See `put_phoenix_layout/2` for more details.

  ## Explicit Template Extensions

  You can provide a template name with an explicit extension such as `"some_template.html"` or
  `"some_template.txt"`. This is set the content of the email respective to the content type of
  the template rendered. For example, if you render an HTML template, the output of the rendering
  will be the HTML content of the email.

  ## Implicit Template Extensions

  You can omit a template's extension and attempt to have both a text template and HTML template
  rendered. To have both types rendered, both templates must share the same base file name. For
  example, if you have a template named `"some_template.txt"` and a template named `"some_template.html"`
  and you call `put_phoenix_template(email, :some_template)`, both templates will be used and will
  set the email content for both content types. The only caveat is *both files must exist*, otherwise you'll
  have an exception raised.

  ## Examples

      iex> put_phoenix_template(email, "some_template.html")
      %Email{content: [%{type: "text/html", value: ...}], ...}

      iex> put_phoenix_template(email, "some_template.txt", name: "John Doe")
      %Email{content: [%{type: "text/plain", value: ...}], ...}

      iex> put_phoenix_template(email, :some_template, user: user)
      %Email{content: [%{type: "text/plain", value: ...}, %{type: "text/html", value: ...}], ...}

  """
  def put_phoenix_template(email, template_name, assigns \\ [])
  @spec put_phoenix_template(t, atom, list()) :: t
  def put_phoenix_template(%Email{} = email, template_name, assigns)
      when is_atom(template_name) do
    with true <- ensure_phoenix_loaded(),
         view_mod <- phoenix_view_module(email),
         layouts <- phoenix_layouts(email),
         template_name <- Atom.to_string(template_name) do
      email
      |> render_html(view_mod, template_name <> ".html", layouts, assigns)
      |> render_text(view_mod, template_name <> ".txt", layouts, assigns)
    end
  end

  @spec put_phoenix_template(t, String.t(), list()) :: t
  def put_phoenix_template(%Email{} = email, template_name, assigns) do
    with true <- ensure_phoenix_loaded(),
         view_mod <- phoenix_view_module(email),
         layouts <- phoenix_layouts(email) do
      case Path.extname(template_name) do
        ".html" ->
          render_html(email, view_mod, template_name, layouts, assigns)

        ".txt" ->
          render_text(email, view_mod, template_name, layouts, assigns)
      end
    end
  end

  defp render_html(email, view_mod, template_name, layouts, assigns) do
    assigns =
      if Map.has_key?(layouts, :html) do
        Keyword.put(assigns, :layout, Map.get(layouts, :html))
      else
        assigns
      end

    html = Phoenix.View.render_to_string(view_mod, template_name, assigns)
    put_html(email, html)
  end

  defp render_text(email, view_mod, template_name, layouts, assigns) do
    assigns =
      if Map.has_key?(layouts, :text) do
        Keyword.put(assigns, :layout, Map.get(layouts, :text))
      else
        assigns
      end

    text = Phoenix.View.render_to_string(view_mod, template_name, assigns)
    put_text(email, text)
  end

  defp ensure_phoenix_loaded do
    unless Code.ensure_loaded?(Phoenix) do
      raise ArgumentError,
            "Attempted to call function that depends on Phoenix. " <>
              "Make sure Phoenix is part of your dependencies"
    end

    true
  end

  defp phoenix_layouts(%Email{__phoenix_layout__: layouts}) do
    layouts = layouts || %{}

    case config(:phoenix_layout) do
      nil ->
        layouts

      {module, layout} when is_atom(module) and is_atom(layout) ->
        configured_layouts = build_layouts({module, layout})
        Map.merge(configured_layouts, layouts)

      _ ->
        raise ArgumentError,
              "Invalid configuration set for :phoenix_layout. " <>
                "Ensure the configuration is a tuple of a module and atom ({MyApp.View, :layout})."
    end
  end

  defp phoenix_view_module(%Email{__phoenix_view__: nil}) do
    mod = config(:phoenix_view)

    unless mod do
      raise ArgumentError,
            "Phoenix view is expected to be set or configured. " <>
              "Ensure your config for :noizu_sendgrid includes a value for :phoenix_view or " <>
              "explicity set the Phoenix view with `put_phoenix_view/2`."
    end

    mod
  end

  defp phoenix_view_module(%Email{__phoenix_view__: view_module}), do: view_module

  @doc """
  Sets the email to be sent with sandbox mode enabled or disabled.

  The sandbox mode will default to what is explicitly configured with
  SendGrid's configuration.
  """
  @spec set_sandbox(t(), boolean()) :: t()
  def set_sandbox(%Email{} = email, enabled?) when is_boolean(enabled?) do
    %Email{email | sandbox: enabled?}
  end

  @doc """
  Transforms an `t:Email.t/0` to a `t:Personalization.t/0`.
  """
  @spec to_personalization(t()) :: Personalization.t()
  def to_personalization(%Email{} = email) do
    %Personalization{
      to: email.to,
      cc: email.cc,
      bcc: email.bcc,
      subject: email.subject,
      substitutions: email.substitutions,
      custom_args: email.custom_args,
      dynamic_template_data: email.dynamic_template_data,
      send_at: email.send_at,
      headers: email.headers
    }
  end

  @doc """
  Adds a `t:Personalization.t/0` to an email.
  """
  @spec add_personalization(t(), Personalization.t()) :: t()
  def add_personalization(%Email{} = email, %Personalization{} = personalization) do
    personalizations = List.wrap(email.personalizations) ++ [personalization]

    %Email{email | personalizations: personalizations}
  end

  defp config(key) do
    Application.get_env(:noizu_sendgrid, key)
  end

  defimpl Jason.Encoder do
    
    defp conditional_insert(params, email, field, as_field \\ nil) do
      cond do
        v = Map.get(email, field) -> put_in(params, [as_field || field], v)
        :else -> params
      end
    end
    
    def encode(%Email{personalizations: [_ | _]} = email, opts) do
      params = %{
                 personalizations: email.personalizations,
                 from: email.from,
                 subject: email.subject,
                 content: email.content,
                 send_at: email.send_at,
                 attachments: email.attachments,
                 headers: email.headers,
               }
               # Template
               |> then(
                    fn(params) ->
                      cond do
                        email.template_id && email.version_id -> put_in(params, [:template_id], email.template_id <> "." <> email.version_id)
                        email.template_id -> put_in(params, [:template_id], email.template_id)
                        email.version_id -> raise ArgumentError, "You must specify template if specifying template version"
                        :else -> params
                      end
                    end)
        # Reply List
               |> then(
                    fn(params) ->
                      cond do
                        email.reply_to_list && email.reply_to ->
                          raise ArgumentError, "You may not set reply_to_list and reply_to at the same time."
                        v = email.reply_to -> put_in(params, [:reply_to], v)
                        v = email.reply_to_list -> put_in(params, [:reply_to_list], v)
                        :else -> params
                      end
                    end)
        # Mail Settings
               |> conditional_insert(email, :mail_settings)
        # Track Settings
               |> conditional_insert(email, :tracking_settings, :track_settings)
        # Categories
               |> conditional_insert(email, :categories)
        # Batch
               |> conditional_insert(email, :batch_id)
        # ASM
               |> conditional_insert(email, :asm)
        # IP Pool
               |> conditional_insert(email, :ip_pool_name)
        # sandbox_mode
               |> then(
                    fn(params) ->
                      # Insure partially populated.
                      params
                      |> update_in([:mail_settings], &(&1 || %{}))
                      |> update_in([:mail_settings, :sandbox_mode], &(&1 || %{}))
                      |> update_in([:mail_settings, :sandbox_mode, :enable], fn(p) ->
                        cond do
                          is_boolean(p) -> p
                          :else -> Application.get_env(:noizu_sendgrid, :sandbox_enable, email.sandbox)
                        end
                      end)
                    end)
      
      Jason.Encode.map(params, opts)
    end

    def encode(%Email{personalizations: nil} = email, opts) do
      personalization = Email.to_personalization(email)

      email
      |> Email.add_personalization(personalization)
      |> encode(opts)
    end
  end
end