<main>

Gmail API Contact form Elixir, Phoenix & Swoosh

G’day

So recently I’ve been working on re-building my blog/portfolio website in Pheonix, and what was a fairly easy straightforward process in React took me quite a long time with Phoenix. Mostly because it’s hard to find good / any examples of what I needed. This being a simple contact me form that sends me an email with the info provided by visitors.

And by the time anyone reads this it will be republished with phoenix, so this contact form is in the website you’re currently on!

I’m fairly new to elixir as it is so following the documentation wasn’t the easiest of things to do but that’s all I had to go off. Got there in the end though, and I thought I’d share it for someone like me in the near future that wished there was just one example to reference.

Firstly install these dependencies:

{:gen_smtp, "~> 1.0"},
{:mail, ">= 0.0.0"},
{:oauth2, "~> 2.0"},

Step 1: Secrets & Token

First things first, Set up your Google Cloud API project. In the credentials section, under Client Secrets, Click the “Download Json” button and save it somewhere in your project. I did this as /static/auth/client_secret.json. Then browse to https://developers.google.com/oauthplayground and find Gmail API v1 and expand it. Then select everything except …/gmail.metadata and click Authorize API’s. Then press Exchange authorization code for tokens, click Auto-refresh the token, then save the Refresh Token in a .txt file located in the same spot as the client_secret.json file. Lastly, up the top right click the cog and tick the Use your own OAuth Credentials and put your client ID & Secret there. Then add https://developers.google.com/oauthplayground to your Authorized redirect URI’s. That’s gmail all set up.

Step 2: Swoosh config & Mailer

For this project I’m using Swoosh with the Swoosh.gmail Adapter. All we have to do to set this up is go to our config.exs file and change the MyProj.Mailer line to this:

config :phoenix_blog, PhoenixBlog.Mailer, adapter: Swoosh.Adapters.Gmail

Then change the line in dev.exs at the bottom from :

config :swoosh, :api_client, false

To:

config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: ProjectName.Finch

This is to enable API’s in development mode, and Finch should be installed in Mix.exs by default. Now in your ProjectName/mailer.ex heres my code for handling the sending of emails:

defmodule ProjectName.Mailer do
  use Swoosh.Mailer, otp_app: :project_name, adapter: Swoosh.Adapters.Gmail
  
  alias ProjectName.Google
  import Swoosh.Email
  
  def send_email(params) do
    access_token = Google.get_access_token()
    email = create_email(params)
    Mailer.deliver(email, adapter: Swoosh.Adapters.Gmail, access_token: access_token )
  end
  
  defp create_email(params) do
    new()
    |> to({"liam", "yourEmailHere@gmail.com"})
    |> from({Map.get(params, "name"), Map.get(params, "email")})
    |> subject("New message from #{Map.get(params, "name")}")
    |> html_body(
      "<h1>Hello</h1>
      <p>#{Map.get(params, "message")}</p>
      "
      )
    |> text_body("#{Map.get(params, "message")} \n")
  end
  
end

In the same directory, create a file called google.ex and paste the following code. (Got this code from a friend, check him out at https://ryandimsey.com) For our use case we don’t need all of these functions though.

defmodule ProjectName.Google do
  @refresh_token_path "priv/static/auth/refresh_token.txt"
  @client_secret_path "priv/static/auth/client_secret.json"
  

  def get_access_token do
    refresh_token()
    |> Map.get(:token)
    |> Map.get(:access_token)
    |> Jason.decode!()
    |> Map.get("access_token")
  end
  
  def refresh_token do
    OAuth2.Client.get_token!(refresh_client())
  end
  
  def refresh_client do
    secret = read_client_info()
    refresh_token = read_refresh_token()
  
    client = OAuth2.Client.new(
      strategy: OAuth2.Strategy.Refresh,
      client_id: Map.get(secret, "client_id"),
      client_secret: Map.get(secret, "client_secret"),
      token_url: Map.get(secret, "token_uri"),
      params: %{"refresh_token" => refresh_token}
    )
    IO.inspect(client)
  
  end
  
  def read_refresh_token do
    File.read!(@refresh_token_path)
  end
  
  def set_refresh_token(nil) do
    IO.puts("Refresh token was nil")
    # Might need to log out and do the full auth from log in
    {:error, "Refresh token wasn't in response"}
  end
  
  def set_refresh_token(refresh_token) do
    File.write!(@refresh_token_path, refresh_token)
    {:info, "Successfully stored refresh token"}
  end
  
  def set_client_secret(client_secret) do
    # Client secret is always needed first so ensure here that the
    # folder is created
    File.mkdir_p!("priv/static/auth")
    File.write!(@client_secret_path, client_secret)
    {:info, "Successfully stored client secret"}
  end
  
  def trade_auth_code_for_tokens!(params \\ []) do
    OAuth2.Client.get_token!(auth_client(), params)
  end
  
  def auth_client do
    secret = read_client_info()
  
    OAuth2.Client.new(
      strategy: OAuth2.Strategy.AuthCode,
      client_id: Map.get(secret, "client_id"),
      client_secret: Map.get(secret, "client_secret"),
      authorize_url: Map.get(secret, "auth_uri"),
      token_url: Map.get(secret, "token_uri"),
      redirect_uri: Map.get(secret, "redirect_uris") |> List.last()
    )
  end
  
  def get_authorize_url!(params \\ []) do
    OAuth2.Client.authorize_url!(auth_client(), params)
  end
  
  # Returns map like the following
  # %{
  #   "client_id" => client_id,
  #   "client_secret" => client_secret,
  #   "redirect_uris" => redirect_uris,
  #   "auth_uri" => auth_uri,
  #   "token_uri" => token_uri
  # }
  defp read_client_info do
    %{"web" => secret} =
      @client_secret_path
      |> File.read!()
      |> Jason.decode!()
  
      IO.inspect(secret)
  
    secret
  end
end

Step 3: The form & Sending the email

Using the default setup from running mix phx.new, inside the home_html.ex file, here is my contact form (havent done much styling to it yet):

  def contact(assigns) do
    assigns = assign(assigns, form: %{} )
  
    ~H"""
    <div id="contact" class="w-full flex flex-col gap-5 items-center mb-10">
      <h2 class="text-white md:text-[3rem] text-[1.5rem] tight">
        Want to get in touch?
      </h2>
      <h3 class="text-gray-500 px-5">
        Fill out the form below and I'll get back to you as soon as i can!
      </h3>
      <.simple_form :let={form} id="contact_form" for={@form} phx-change="update"               action={~p"/send_email"}  class=" text-gray-200">
        <.input field={form[:name]} type="text" label="Name" />
        <.input field={form[:email]} type="text" label="Email" />
        <.input field={form[:message]} type="textarea" label="Message" />
        <:actions>
          <.button class="bg-slate-700">Send Email</.button>
        </:actions>
      </.simple_form>
    </div>
    """
  end

Then in Router.ex we save the endpoint for our form:

post "/send_email", HomeController, :send_email

I just added this under the “/“ scope. Then in HomeController, add the send_email function and ailias in the Mailer:

defmodule ProjectNameWeb.HomeController do
  use ProjectNameWeb, :controller
  
  alias ProjectName.Posts
  alias ProjectName.Posts.Post
  alias ProjectName.Mailer

  def home(conn, _params) do
    # The home page is often custom made,
    # so skip the default app layout.
    posts = Posts.list_posts()
    render(conn, :home, layout: false, posts: posts)
  end
  
  def send_email(conn, params) do
    case Mailer.send_email(params) do
      {:ok, _ } ->
        conn
        |> put_flash(:info, "Email sent")
        |> redirect(to: ~p"/")
  
      {:error, _ } ->
        conn
        |> put_flash(:error, "Email not sent, Please try again later, or email me directly at yourEmailHere@gmail.com")
        |> redirect(to: ~p"/")
    end
  end
end

And that’s it!

Without using live views or some custom javascript, there’s not much choice but for the page having to refresh, as a controller function must return a new render or redirect.. to my knowledge anyways. I’ll be playing around with that in the future.

If I missed anything or you have any questions, don’t hesitate to reach out.

</main>