Current section

Files

Jump to
bamboo_gmail lib bamboo adapters gmail_adapter.ex
Raw

lib/bamboo/adapters/gmail_adapter.ex

defmodule Bamboo.GmailAdapter do
@moduledoc """
Sends email using the Gmail API with OAuth2 authentication
There are a few preconditions that must be met before this adapter can be used to send email:
1. Admin access to a GSuite account
2. Implement [server-side authorization](https://developers.google.com/gmail/api/auth/web-server)
3. Grant the service account domain-wide authority
4. Authorize API client with required scopes
Some application settings must be configured. See the example section below.
---
## Config Settings
#### Required GmailAdapter settings:
`adapter`: Bamboo adapter
`sub`: Email address the service account is impersonating (address the email is sent from).
- If impersonation is not needed, then `nil` (it is likely needed).
#### Required Dependency settings:
`json`: Google auth crendentials must be povided in JSON format.
- These are generated in the [Google Developers Console](https://console.developers.google.com/)
#### Optional settings:
`sandbox`: development mode that does not send email.
- details of the API call are instead output to the elixir console.
#### Note:
*Secrets such as the service account sub, and the auth credentials should not
be commited to version control.*
Instead, pass in via environment variables using a tuple: `{:system, "SUB_ADDRESS"}`,
or read in from a file: `"creds.json" |> File.read!`
---
## Example Config
config :app_name, GmailAdapterTestWeb.Mailer,
adapter: Bamboo.GmailAdapter,
sub: {:system, "SUB_ADDRESS"},
sandbox: false
# Google auth credentials must be provided to the `goth` app
config :goth, json: {:system, "GCP_CREDENTIALS"}
---
## Google Authorization/Authentication Help
The Google-related preconditions described above may be a little tricky.
If you find yourself stuck, please refer to the [wiki]() for help.
"""
import Bamboo.GmailAdapter.RFC2822, only: [render: 1]
alias Bamboo.GmailAdapter.Errors.{ConfigError, TokenError, HTTPError}
@gmail_auth_scope "https://www.googleapis.com/auth/gmail.send"
@gmail_send_endpoint "https://www.googleapis.com/gmail/v1/users/me/messages/send"
@behaviour Bamboo.Adapter
def deliver(email, config) do
handle_dispatch(email, config)
end
def handle_config(config) do
validate_config_fields(config)
end
# TODO: Handle attachments
def supports_attachments?, do: true
defp handle_dispatch(email, config = %{sandbox: true}) do
log_to_sandbox(config, label: "config")
log_to_sandbox(email, label: "email")
build_message(email)
|> log_to_sandbox(label: "MIME message")
|> Base.url_encode64()
|> log_to_sandbox(label: "base64url encoded message")
get_sub(config)
|> get_access_token()
|> log_to_sandbox(label: "access token")
end
defp handle_dispatch(email, config) do
message =
build_message(email)
|> Base.url_encode64()
get_sub(config)
|> get_access_token()
|> make_request(message)
end
defp build_message(email) do
Mail.build_multipart()
|> put_to(email)
|> put_cc(email)
|> put_bcc(email)
|> put_from(email)
|> put_subject(email)
|> put_html_body(email)
|> put_text_body(email)
|> put_attachments(email)
|> render()
end
defp put_to(message, %{to: recipients}) do
recipients = Enum.map(recipients, fn {_, email} -> email end)
Mail.put_to(message, recipients)
end
defp put_cc(message, %{cc: recipients}) do
recipients = Enum.map(recipients, fn {_, email} -> email end)
Mail.put_cc(message, recipients)
end
defp put_bcc(message, %{bcc: recipients}) do
recipients = Enum.map(recipients, fn {_, email} -> email end)
Mail.put_bcc(message, recipients)
end
defp put_from(message, %{from: {_, sender}}) do
Mail.put_from(message, sender)
end
defp put_subject(message, %{subject: subject}) do
Mail.put_subject(message, subject)
end
defp put_html_body(message, %{html_body: nil}), do: message
defp put_html_body(message, %{html_body: html_body}) do
Mail.put_html(message, html_body)
end
defp put_text_body(message, %{text_body: nil}), do: message
defp put_text_body(message, %{text_body: text_body}) do
Mail.put_text(message, text_body)
end
# TODO: Implement
defp put_attachments(message, _attachment), do: message
defp make_request(token, message) do
headers = build_request_headers(token)
body = build_request_body(message)
case HTTPoison.post(@gmail_send_endpoint, body, headers) do
{:ok, response} -> response
{:error, error} -> handle_error(:http, error)
end
end
# Right now `sub` is the only required field.
# TODO: Generalize this function
defp validate_config_fields(config = %{sub: _}), do: config
defp validate_config_fields(_no_match) do
handle_error(:conf, "sub")
end
defp get_sub(%{sub: sub}) do
case sub do
{:system, s} -> validate_env_var(s)
_ -> sub
end
end
defp validate_env_var(env_var) do
case var = System.get_env(env_var) do
nil -> handle_error(:env, "Environment variable '#{env_var}' not found")
_ -> var
end
end
defp get_access_token(sub) do
case Goth.Token.for_scope(@gmail_auth_scope, sub) do
{:ok, token} -> Map.get(token, :token)
{:error, error} -> handle_error(:auth, error)
end
end
defp handle_error(scope, error) do
case scope do
:auth -> raise TokenError, message: error
:http -> raise HTTPError, message: error
:conf -> raise ConfigError, field: error
:env -> raise ArgumentError, message: error
end
end
defp build_request_headers(token) do
[Authorization: "Bearer #{token}", "Content-Type": "application/json"]
end
defp build_request_body(message) do
"{\"raw\": \"#{message}\"}"
end
defp log_to_sandbox(entity, label: label) do
IO.puts("[sandbox] <#{label}> #{inspect(entity)}\n")
entity
end
end