Skip to content

Instantly share code, notes, and snippets.

@mark-kubacki
Last active August 29, 2015 14:05
Show Gist options
  • Save mark-kubacki/73ee2abe30e13af93a18 to your computer and use it in GitHub Desktop.
Save mark-kubacki/73ee2abe30e13af93a18 to your computer and use it in GitHub Desktop.
how to accept incoming email utilizing services of Mailgun
func BindHexEncodedField(field *[]byte) func(string, []string, binding.Errors) binding.Errors {
return func(fieldName string, formVals []string, errs binding.Errors) binding.Errors {
var err error
*field, err = hex.DecodeString(formVals[0])
if err != nil {
errs.Add([]string{fieldName}, binding.DeserializationError, err.Error())
}
return errs
}
}
func BindCustomDateField(field *time.Time, layout string) func(string, []string, binding.Errors) binding.Errors {
return func(fieldName string, formVals []string, errs binding.Errors) binding.Errors {
var err error
*field, err = time.Parse(layout, formVals[0])
if err != nil {
errs.Add([]string{fieldName}, binding.DeserializationError, err.Error())
}
return errs
}
}
func BindJsonEncodedField(field interface{}) func(string, []string, binding.Errors) binding.Errors {
return func(fieldName string, formVals []string, errs binding.Errors) binding.Errors {
err := json.Unmarshal([]byte(formVals[0]), &field)
if err != nil {
errs.Add([]string{fieldName}, binding.DeserializationError, err.Error())
}
return errs
}
}
// receiving of emails
type MailgunParsedMessage struct {
ContentType string
Date time.Time
From string
InReplyTo string
MessageId string
MimeVersion string
Received string
References string
Sender string
Subject string
To string
UserAgent string
XMailgunVariables map[string]string
BodyHtml string
BodyPlain string
ContentIdMap map[string]string
EnvelopeFrom string
MessageHeaders [][]string
Recipient string
EnvelopeSender string
Signature []byte
StrippedHtml string
StrippedSignature string
StrippedText string
Timestamp uint64
Token string
AttachmentCount uint8
Attachments map[string][]*multipart.FileHeader
}
func (f *MailgunParsedMessage) FieldMap() binding.FieldMap {
return binding.FieldMap{
&f.ContentType: "Content-Type",
&f.From: "From",
&f.InReplyTo: "In-Reply-To",
&f.MessageId: "Message-Id",
&f.MimeVersion: "Mime-Version",
&f.Received: "Received",
&f.References: "References",
&f.Sender: "Sender",
&f.Subject: "Subject",
&f.To: "To",
&f.UserAgent: "User-Agent",
&f.BodyHtml: "body-html",
&f.BodyPlain: "body-plain",
&f.EnvelopeFrom: "from",
&f.Recipient: "recipient",
&f.EnvelopeSender: "sender",
&f.StrippedHtml: "stripped-html",
&f.StrippedSignature: "stripped-signature",
&f.StrippedText: "stripped-text",
&f.Timestamp: "timestamp",
&f.Token: "token",
&f.AttachmentCount: "attachment-count",
&f.Signature: binding.Field{
Form: "signature",
Binder: BindHexEncodedField(&f.Signature),
},
&f.Date: binding.Field{
Form: "Date",
Binder: BindCustomDateField(&f.Date, time.RFC1123Z),
},
&f.ContentIdMap: binding.Field{
Form: "content-id-map",
Binder: BindJsonEncodedField(&f.ContentIdMap),
},
&f.XMailgunVariables: binding.Field{
Form: "X-Mailgun-Variables",
Binder: BindJsonEncodedField(&f.XMailgunVariables),
},
&f.MessageHeaders: binding.Field{
Form: "message-headers",
Binder: BindJsonEncodedField(&f.MessageHeaders),
},
}
}
// see http://documentation.mailgun.com/user_manual.html#securing-webhooks
func (f *MailgunParsedMessage) CheckMAC() bool {
mac := hmac.New(sha256.New, []byte(mailgunApiKey))
mac.Write([]byte(strconv.FormatUint(f.Timestamp, 10)))
mac.Write([]byte(f.Token))
expectedMAC := mac.Sum(nil)
return hmac.Equal(f.Signature, expectedMAC)
}
// Yields an error if the signature doesn't match.
func (f MailgunParsedMessage) Validate(req *http.Request, errs binding.Errors) binding.Errors {
if !f.CheckMAC() {
fmt.Println(mailgunApiKey, string(f.Timestamp), f.Token, f.Signature)
errs = append(errs, binding.Error{
FieldNames: []string{"signature", "timestamp", "token", "private-api-key"},
Classification: "HMAC Failure",
Message: "The given signature does not match HMAC.",
})
}
return errs
}
func receiveEmail(w http.ResponseWriter, req *http.Request) {
fmt.Println("remote address:", req.RemoteAddr)
parsedMail := new(MailgunParsedMessage)
errs := binding.Bind(req, parsedMail)
if errs != nil {
errOutput, _ := json.Marshal(errs)
fmt.Println(string(errOutput))
return
}
fmt.Println(parsedMail.From, parsedMail.To, parsedMail.Date.UTC(), parsedMail.Subject)
// attachments
if req.MultipartForm != nil {
parsedMail.Attachments = req.MultipartForm.File
} // else we didn't get the input as MimeMultipart
// is the sender whitelisted?
if not … {
w.WriteHeader(406)
fmt.Fprint(w, "The sender is not on the whitelist. Try sending from your other email account?")
return
}
// is the recipient known?
if not … {
w.WriteHeader(406)
fmt.Fprint(w, "Sorry, unknown recipient.")
return
}
// do something with the email
}
@mark-kubacki
Copy link
Author

:WARNING: Although this example works, its purpose is to illustrate shortcomings of https://github.com/mholt/binding which we want to address.

Follow:

  1. mholt/binding#17
  2. mholt/binding#18

Exemplary: You could make MailgunParsedMessage.Signature a standalone type—which wouldn't require any additional Binder—like this:

type SignatureField []byte

func (s *SignatureField) Bind(fieldName string, strVals []string, errs binding.Errors) binding.Errors {
    decoded, err := hex.DecodeString(strVals[0])
    if err != nil {
        errs.Add([]string{fieldName}, binding.DeserializationError, err.Error())
    } else {
        *s = SignatureField(decoded)
    }
    return errs
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment