Documentation
¶
Overview ¶
Package contact turns a submitted form into a validated message and hands it to something that can deliver it.
Index ¶
Examples ¶
Constants ¶
const ( MaxNameLen = 100 MaxEmailLen = 254 // RFC 5321 maximum for a forward path MaxMessageLen = 5000 MinMessageLen = 10 )
Field limits. Generous for a human, mean for anything pasting a payload: the message cap is the one that matters, since an unbounded body is how a form becomes a way to post arbitrary volumes of text into someone's inbox.
const DefaultSubject = "Contact form: {{ .Name }}"
DefaultSubject is what a form that does not name a subject template gets.
Variables ¶
var ErrSpam = errors.New("submission looks automated")
ErrSpam means the submission looked automated. Callers should answer as if it succeeded: telling a bot it was caught only teaches it what to change.
Functions ¶
func Undeliverable ¶ added in v1.2.0
Undeliverable wraps err as a delivery failure. Adapters use it so every failure that leaves a Mailer looks the same from the outside.
Types ¶
type DeliveryError ¶ added in v1.2.0
type DeliveryError struct {
// Provider is the adapter that failed: "smtp" or "mailgun".
Provider string
// Op is the step it failed at — "dial", "auth", "send". Named for what the
// adapter was doing rather than for the function it was in, because this
// ends up in a log somebody reads at three in the morning.
Op string
Err error
}
DeliveryError is what a Mailer returns when it could not deliver.
It lives here rather than beside either adapter because it belongs to the port: the handler is what reads it, and the handler must not have to know whether the form behind it speaks SMTP or HTTP. Once two providers can fail, "could not send message" in a log line stops being enough to act on — which one, and at which step, is the difference between a wrong password and a mail server that is briefly down.
func (*DeliveryError) Error ¶ added in v1.2.0
func (e *DeliveryError) Error() string
func (*DeliveryError) Unwrap ¶ added in v1.2.0
func (e *DeliveryError) Unwrap() error
Unwrap keeps errors.Is and errors.As working through this, so an adapter wrapping a provider's own typed error does not hide it.
type Form ¶
type Form struct {
// ID is the last path segment of the endpoint, so it has to survive being
// in a URL. Validated by whatever builds the Form.
ID string
// Origins are the sites allowed to post to this form. Per form rather than
// global: one site being allowed to use its own form must not let it use
// somebody else's.
Origins []string
// Subject is a text/template rendered with .Name, .Email and .Form. Empty
// means DefaultSubject.
Subject string
// RatePerHour is submissions allowed per client address per hour. Zero
// disables the limit.
RatePerHour int
}
Form is one configured form, in the terms this package needs: who may post to it, how often, and what the resulting subject line says. Where the mail goes is not here — that belongs to the Mailer the form is wired to, so a form and its destination are chosen together at the composition root.
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler serves one form's endpoint. One per configured form, each with its own origins, its own subject line, its own rate limit and its own Mailer — which is what keeps two forms on one service from leaking into each other.
func NewHandler ¶
NewHandler wires a handler for one form.
Origins are the sites allowed to post here; a browser will not send the form from anywhere else once this is set, and anything that is not a browser was never going to respect CORS anyway — so this is about keeping other people's pages from using our mailbox, not about authentication.
It returns an error rather than panicking on a bad subject template, because that template comes from a config file a person edits, and the useful moment to hear about a typo in it is at startup.
type Mailer ¶
Mailer is what this package needs from the outside world: somewhere to send a message. Declared here rather than beside the SMTP implementation, because the consumer is what knows the shape it wants.
type Message ¶
Message is what actually gets delivered. Separate from Submission on purpose: a submission is untrusted input, a message has been through Validate.
Subject is left empty by Validate and filled in by the Handler, because the subject line belongs to the form that was posted to rather than to the submission: two forms on the same service word it differently.
func Validate ¶
func Validate(s Submission) (Message, error)
Validate checks a submission and returns the message to deliver.
Trims first, then measures. " " in a required field is absence with extra steps, and counting it as present lets an empty message through.
Example ¶
The ordinary path: an untrusted Submission goes in, a Message that has been through validation comes out, trimmed and with the address parsed.
The subject is not filled in here. It belongs to the form that was posted to — two forms on one service word it differently — so the Handler renders it from that form's template.
package main
import (
"fmt"
"github.com/alrayyes/form-handler/internal/contact"
)
func main() {
msg, err := contact.Validate(contact.Submission{
Name: " Ada Lovelace ",
Email: "ada@example.com",
Message: "Please get in touch about an awkward system.",
})
if err != nil {
fmt.Println("rejected:", err)
return
}
fmt.Printf("%q\n", msg.Name)
fmt.Println(msg.Email)
fmt.Printf("subject: %q\n", msg.Subject)
}
Output: "Ada Lovelace" ada@example.com subject: ""
Example (Honeypot) ¶
A caught bot is an error, but not one to show anyone: answer it exactly as you would a real submission, because telling it which field gave it away only teaches whoever wrote it what to leave alone next time.
package main
import (
"errors"
"fmt"
"github.com/alrayyes/form-handler/internal/contact"
)
func main() {
_, err := contact.Validate(contact.Submission{
Name: "Bot",
Email: "bot@example.com",
Message: "Cheap watches, buy now please.",
Website: "http://spam.example",
})
fmt.Println(errors.Is(err, contact.ErrSpam))
}
Output: true
Example (ValidationError) ¶
A rejected field says which one and why, so the browser can point at it rather than showing a generic failure.
package main
import (
"errors"
"fmt"
"github.com/alrayyes/form-handler/internal/contact"
)
func main() {
_, err := contact.Validate(contact.Submission{
Name: "Ada Lovelace",
Email: "not-an-address",
Message: "Please get in touch about an awkward system.",
})
var ve contact.ValidationError
if errors.As(err, &ve) {
fmt.Println(ve.Field, "->", ve.Reason)
}
}
Output: email -> not a valid address
type Submission ¶
type Submission struct {
Name string `json:"name"`
Email string `json:"email"`
Message string `json:"message"`
// Website is a honeypot. It is hidden from people and left empty by them;
// a bot that fills every field it finds will populate it. Named something
// a bot would want to fill rather than "honeypot".
Website string `json:"website"`
}
Submission is the raw form, exactly as posted.
type ValidationError ¶
ValidationError names the field that was wrong, so the browser can point at it instead of showing a generic failure.
func (ValidationError) Error ¶
func (e ValidationError) Error() string