formailer

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 6, 2022 License: MPL-2.0 Imports: 21 Imported by: 0

README

Formailer

Go

Screenshot

If you need your contact form to send you an email from your Jamstack site, Formailer is the serverless library for you! Out of the box Formailer supports redirects, reCAPTCHA, custom email templates, and custom handlers.

Quickstart

View Documenation

Formailer tries to require as little boilerplate as possible. Create a form, add some emails, and run a handler.

import (
	"github.com/djatwood/formailer"
	"github.com/djatwood/formailer/handlers"
	
	// For Netlify
	"github.com/aws/aws-lambda-go/lambda"
)

func main() {
	contact := formailer.New("Contact")
	contact.AddEmail(formailer.Email{
		To:      "info@domain.com",
		From:    `"Company" <noreply@domain.com>`,
		Subject: "New Contact Submission",
	})

	// Vercel
	handlers.Vercel(formailer.DefaultConfig, w, r)
	// Netlify
	lambda.Start(handlers.Netlify(formailer.DefaultConfig))
}

If you want to use your own handler that's not a problem either. View an example handler.

Then you update your form. So that we can use the correct form config you need to add the form id as a hidden input with the name _form_name. Note the form id is case-insensitive.

Formailer supports submitting forms as application/x-www-form-urlencoded, multipart/form-data, or application/json.

The built-in handlers come with built in Google reCaptcha verification if you add a RECAPTCHA_SECRET to your environment variables.

<!-- html form -->
<input type="hidden" name="_form_name" value="contact">
<button class="g-recaptcha" data-sitekey="reCAPTCHA_site_key" data-callback='onSubmit' data-action='submit'>Submit</button>
// JSON object
{
	...
	"_form_name": "contact",
}

Customization

You can customize Formailer to suit your needs. You can add as many forms as you'd like. As long as they have unique ids. Each form can have it's own email template and SMTP settings. But if you want to set defaults for everything you can.

SMTP

All of your SMTP variables must be saved in the environment. You can add as many configs as you have emails. And you can save a default config to fallback on. Note that if you have default config you don't need to specify every option again. Any missing options will fallback to the default.

If you build your own handler you can store the config anywhere you want. Just pass a *mail.SMTPServer to submission.Send(server) and you're good to go.

# Default
SMTP_HOST=mail.example.com
SMTP_PORT=587
SMTP_USER=noreply@example.com
SMTP_PASS=mysupersecretpassword

# Overrides
# _HOST and _PORT will fallback to the default above
SMTP_EMAIL-ID_USER=support@example.com
SMTP_EMAIL-ID_PASS=youcantguessthispassword
Templates

Here is the default template.

Screenshot

You can override this template on any form by using the Template field. You can use Go 1.16 >= embed package to separate your template files from your function file.

contact.AddEmail(formailer.Email{
	...
	Template: defaultTemplate,
}

//go:embed mytemplate.html
var defaultTemplate string

// OR

defaultTemplate := `
<html>
<head>
    <style>
        h3 {
            color: #000;
            margin-bottom: 5px;
        }

        p {
            color: #333;
            margin-bottom: 2rem;
        }
    </style>
</head>
<body>
    {{ range $name, $value := .Values }}
    <h3>{{$name}}</h3>
    <p>{{$value}}</p>
    {{ end }}
</body>
</html>
`
Custom Handlers

Formailer ships with Netlify and Vercel handlers but if you need more control over the data. Or would like to run on a different platform, it's not too difficult to get setup. Here is a template to get you started.

func Handler(w http.ResponseWriter, r *http.Request) {
	// pre-processing, check HTTP method

	// convert body from io.Reader to string
	body := new(strings.Builder)
	_, err := io.Copy(body, r.Body)
	if err != nil {
		// handle error
		return
	}
	
	// Parse body
	submission, err := formailer.Parse(r.Header.Get("Content-Type"), body.String())
	if err != nil {
		// handle error
		return
	}

	// manipulate data, check honey pot fields
	// handlers.VerifyRecaptcha()

	// Send emails
	err = submission.Send()
	if err != nil {
		// handle error
		return
	}

	// handle success
}

Why did I buid Formailer?

I love Jamstack but SaaS can get expensive pretty quickly. Netlify has a built in form system that costs $19/month after the first 100 submissions. It also has a serverless function system that allows for 125k invocations a month. So I did the obvious thing, create a library that handles forms for Jamstack sites.

The challenge

Netlify barely supports Go, you can't even use the Netlify CLI to test Go functions. Every change had to be commited and tested directly on Netlify. Even worse is that I had minimal experience working with multipart forms before this project. And my testing software Hoppscotch doesn't implement multipart forms in a traditional way which led to a bunch of builds that I thought didn't work but actually did.

There's also an annoying bug with environment variables where functions can't read variabes defined in the netlify.toml. So you'll just have to add them all in the Netlify UI.

Later I switched to Vercel for testing which was a huge breath of fresh air. You can test Go functions locally even though Go support is still in alpha.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var DefaultConfig = make(Config)

DefaultConfig is the config used when using functions New, Add, and Parse. This helps keep boilerplate code to a minimum.

Functions

func Add

func Add(forms ...*Form)

Add adds forms to the default config. It allows you to add forms without the default settings provided by New.

Types

type Attachment

type Attachment struct {
	Filename string
	MimeType string
	Data     []byte
}

Attachment contains file data for an email attachment

type Config

type Config map[string]*Form

Config is a map of Forms used when parsing a submission to load the correct form settings and emails.

func (Config) Add

func (c Config) Add(forms ...*Form)

Add adds forms to the config falling back on Name if ID is not set.

func (Config) Parse

func (c Config) Parse(contentType string, body string) (*Submission, error)

Parse creates a submission parsing the data based on the Content-Type header. Setting Submission.Form based on the _form_name field and removing any ignored fields from Submisson.Order.

type Email

type Email struct {
	// ID is used when looking up SMTP settings.
	// It is case-insensitive but will be matched as UPPERCASE. ex: SMTP_FORM-ID_HOST.
	ID string

	To      string
	From    string
	Cc      []string
	Bcc     []string
	ReplyTo string
	Subject string

	// Template is a go html template to be used when generating the email.
	Template string
}

Email contains all the setting to send an email

func (*Email) Email

func (e *Email) Email(submission *Submission) (*mail.Email, error)

Email returns a *mail.Email generating the message with the provided submission

func (*Email) Send

func (e *Email) Send(email *mail.Email) error

Send sends the provided email

type Form

type Form struct {
	// ID is a case-insensitive string used to look up forms while parsing a submission. It will be matched to a submission's _form_name field.
	// If ID is not set, a case-insensitive version of Name will be used for matching instead.
	ID string

	// Name is a way to store a "Pretty" version of the form ID.
	Name string

	// Emails is a list of emails. Generally you want to use the AddEmail method instead of adding emails directly.
	Emails []Email

	// Redirect is used when with the default handlers to return 303 See Other and points the browser to the set value.
	Redirect string

	// When ReCAPTCHA is set to true the default handlers with verify the g-recaptcha-response field.
	ReCAPTCHA bool
	// contains filtered or unexported fields
}

Form is for settings that should be set per submission but not per email. Such as redirects and ReCAPTCHA.

func New

func New(id string) *Form

New creates a new Form and adds it to the default config. It also automatically sets the name to the ID and adds ignores the form name and recaptcha fields.

func (*Form) AddEmail

func (f *Form) AddEmail(emails ...Email)

AddEmail adds emails to the form.

func (*Form) Ignore

func (f *Form) Ignore(fields ...string)

Ignore updates the Form.ignore map

type Submission

type Submission struct {
	// Form is the form this submission submitted as.
	Form *Form

	// Order is a list of the fields in the original order of submisson.
	// Maps in go are sorted alphabetically which causes readability issues in the generated emails. Note only first level elements are ordered.
	// The fields set using Form.Ignore will be removed from this list.
	Order []string

	// Values contains the submitted form data.
	Values map[string]interface{}

	// Attachments is a list of files to be attached to the email
	Attachments []Attachment
}

Submission is the unmarshaled version on the form submission. It contains the submitted values and the form settings needed for sending emails.

func Parse

func Parse(contentType, body string) (*Submission, error)

Parse creates a submission using the default config.

func (*Submission) Send

func (s *Submission) Send() error

Send sends all the emails for this form

Directories

Path Synopsis
examples
gcf
hbs

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL