We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
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.