Skip to content

Commit

Permalink
Merge pull request #218 from thrawn01/thrawn/develop
Browse files Browse the repository at this point in the history
ValidateEmail() now supports v3 and v4
  • Loading branch information
thrawn01 authored May 5, 2020
2 parents 263140a + bae4a2b commit 6279ab3
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 110 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [4.1.0] - 2020-04-23
### Changed
* Added EmailVerification.reason is now a []string (Fixes #217)

## [4.0.1] - 2020-03-10
### Added
* Added SetTemplateVersion and SetTemplateRenderText methods to Message
Expand Down
195 changes: 99 additions & 96 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()

// Send the message with a 10 second timeout
// Send the message with a 10 second timeout
resp, id, err := mg.Send(ctx, message)

if err != nil {
Expand Down Expand Up @@ -71,41 +71,41 @@ func main() {
// (https://app.mailgun.com/app/account/security)
mg := mailgun.NewMailgun("your-domain.com", "your-private-key")

it := mg.ListEvents(&mailgun.ListEventOptions{Limit: 100})

var page []mailgun.Event

// The entire operation should not take longer than 30 seconds
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()

// For each page of 100 events
for it.Next(ctx, &page) {
for _, e := range page {
// You can access some fields via the interface
fmt.Printf("Event: '%s' TimeStamp: '%s'\n", e.GetName(), e.GetTimestamp())

// and you can act upon each event by type
switch event := e.(type) {
case *events.Accepted:
fmt.Printf("Accepted: auth: %t\n", event.Flags.IsAuthenticated)
case *events.Delivered:
fmt.Printf("Delivered transport: %s\n", event.Envelope.Transport)
case *events.Failed:
fmt.Printf("Failed reason: %s\n", event.Reason)
case *events.Clicked:
fmt.Printf("Clicked GeoLocation: %s\n", event.GeoLocation.Country)
case *events.Opened:
fmt.Printf("Opened GeoLocation: %s\n", event.GeoLocation.Country)
case *events.Rejected:
fmt.Printf("Rejected reason: %s\n", event.Reject.Reason)
case *events.Stored:
fmt.Printf("Stored URL: %s\n", event.Storage.URL)
case *events.Unsubscribed:
fmt.Printf("Unsubscribed client OS: %s\n", event.ClientInfo.ClientOS)
}
}
}
it := mg.ListEvents(&mailgun.ListEventOptions{Limit: 100})

var page []mailgun.Event

// The entire operation should not take longer than 30 seconds
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()

// For each page of 100 events
for it.Next(ctx, &page) {
for _, e := range page {
// You can access some fields via the interface
fmt.Printf("Event: '%s' TimeStamp: '%s'\n", e.GetName(), e.GetTimestamp())

// and you can act upon each event by type
switch event := e.(type) {
case *events.Accepted:
fmt.Printf("Accepted: auth: %t\n", event.Flags.IsAuthenticated)
case *events.Delivered:
fmt.Printf("Delivered transport: %s\n", event.Envelope.Transport)
case *events.Failed:
fmt.Printf("Failed reason: %s\n", event.Reason)
case *events.Clicked:
fmt.Printf("Clicked GeoLocation: %s\n", event.GeoLocation.Country)
case *events.Opened:
fmt.Printf("Opened GeoLocation: %s\n", event.GeoLocation.Country)
case *events.Rejected:
fmt.Printf("Rejected reason: %s\n", event.Reject.Reason)
case *events.Stored:
fmt.Printf("Stored URL: %s\n", event.Storage.URL)
case *events.Unsubscribed:
fmt.Printf("Unsubscribed client OS: %s\n", event.ClientInfo.ClientOS)
}
}
}
}
```

Expand All @@ -126,18 +126,18 @@ func main() {
// (https://app.mailgun.com/app/account/security)
mg := mailgun.NewMailgun("your-domain.com", "your-private-key")

begin := time.Now().Add(time.Second * -3)
begin := time.Now().Add(time.Second * -3)

// Very short poll interval
it := mg.PollEvents(&mailgun.ListEventOptions{
// Only events with a timestamp after this date/time will be returned
Begin: &begin,
// How often we poll the api for new events
PollInterval: time.Second * 30,
})
// Very short poll interval
it := mg.PollEvents(&mailgun.ListEventOptions{
// Only events with a timestamp after this date/time will be returned
Begin: &begin,
// How often we poll the api for new events
PollInterval: time.Second * 30,
})

ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Poll until our email event arrives
var page []mailgun.Event
Expand Down Expand Up @@ -170,11 +170,14 @@ import (
var apiKey string = "your-api-key"

func main() {
// To use the /v4 version of validations define MG_URL in the envronment
// as `https://api.mailgun.net/v4` or set `v.SetAPIBase("https://api.mailgun.net/v4")`

// Create an instance of the Validator
v := mailgun.NewEmailValidator(apiKey)

ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()

email, err := v.ValidateEmail(ctx, "[email protected]", false)
if err != nil {
Expand Down Expand Up @@ -206,50 +209,50 @@ func main() {
// (https://app.mailgun.com/app/account/security)
mg := mailgun.NewMailgun("your-domain.com", "private-api-key")

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {

var payload mailgun.WebhookPayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
fmt.Printf("decode JSON error: %s", err)
w.WriteHeader(http.StatusNotAcceptable)
return
}

verified, err := mg.VerifyWebhookSignature(payload.Signature)
if err != nil {
fmt.Printf("verify error: %s\n", err)
w.WriteHeader(http.StatusNotAcceptable)
return
}

if !verified {
w.WriteHeader(http.StatusNotAcceptable)
fmt.Printf("failed verification %+v\n", payload.Signature)
return
}

fmt.Printf("Verified Signature\n")

// Parse the event provided by the webhook payload
e, err := mailgun.ParseEvent(payload.EventData)
if err != nil {
fmt.Printf("parse event error: %s\n", err)
return
}

switch event := e.(type) {
case *events.Accepted:
fmt.Printf("Accepted: auth: %t\n", event.Flags.IsAuthenticated)
case *events.Delivered:
fmt.Printf("Delivered transport: %s\n", event.Envelope.Transport)
}
})

fmt.Println("Serve on :9090...")
if err := http.ListenAndServe(":9090", nil); err != nil {
fmt.Printf("serve error: %s\n", err)
os.Exit(1)
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {

var payload mailgun.WebhookPayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
fmt.Printf("decode JSON error: %s", err)
w.WriteHeader(http.StatusNotAcceptable)
return
}

verified, err := mg.VerifyWebhookSignature(payload.Signature)
if err != nil {
fmt.Printf("verify error: %s\n", err)
w.WriteHeader(http.StatusNotAcceptable)
return
}

if !verified {
w.WriteHeader(http.StatusNotAcceptable)
fmt.Printf("failed verification %+v\n", payload.Signature)
return
}

fmt.Printf("Verified Signature\n")

// Parse the event provided by the webhook payload
e, err := mailgun.ParseEvent(payload.EventData)
if err != nil {
fmt.Printf("parse event error: %s\n", err)
return
}

switch event := e.(type) {
case *events.Accepted:
fmt.Printf("Accepted: auth: %t\n", event.Flags.IsAuthenticated)
case *events.Delivered:
fmt.Printf("Delivered transport: %s\n", event.Envelope.Transport)
}
})

fmt.Println("Serve on :9090...")
if err := http.ListenAndServe(":9090", nil); err != nil {
fmt.Printf("serve error: %s\n", err)
os.Exit(1)
}
}
```

Expand Down Expand Up @@ -288,14 +291,14 @@ func main() {
recipient := "[email protected]"

// The message object allows you to add attachments and Bcc recipients
message := mg.NewMessage(sender, subject, body, recipient)
message.SetTemplate("passwordReset")
message.AddTemplateVariable("passwordResetLink", "some link to your site unique to your user")
message := mg.NewMessage(sender, subject, body, recipient)
message.SetTemplate("passwordReset")
message.AddTemplateVariable("passwordResetLink", "some link to your site unique to your user")

ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()

// Send the message with a 10 second timeout
// Send the message with a 10 second timeout
resp, id, err := mg.Send(ctx, message)

if err != nil {
Expand Down
54 changes: 49 additions & 5 deletions email_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,21 @@ type EmailVerification struct {
IsDisposableAddress bool `json:"is_disposable_address"`
// Indicates whether Mailgun thinks the address is an email distribution list.
IsRoleAddress bool `json:"is_role_address"`
// A human readable reason the address is reported as invalid
// The reason why a specific validation may be unsuccessful. (Available in the V3 response)
Reason string `json:"reason"`
// A list of potential reasons why a specific validation may be unsuccessful. (Available in the v4 response)
Reasons []string
}

type v4EmailValidationResp struct {
IsValid bool `json:"is_valid"`
MailboxVerification string `json:"mailbox_verification"`
Parts EmailVerificationParts `json:"parts"`
Address string `json:"address"`
DidYouMean string `json:"did_you_mean"`
IsDisposableAddress bool `json:"is_disposable_address"`
IsRoleAddress bool `json:"is_role_address"`
Reason []string `json:"reason"`
}

type addressParseResult struct {
Expand Down Expand Up @@ -134,8 +147,15 @@ func (m *EmailValidatorImpl) getAddressURL(endpoint string) string {
}

// ValidateEmail performs various checks on the email address provided to ensure it's correctly formatted.
// It may also be used to break an email address into its sub-components. (See example.)
// It may also be used to break an email address into its sub-components. If user has set the
func (m *EmailValidatorImpl) ValidateEmail(ctx context.Context, email string, mailBoxVerify bool) (EmailVerification, error) {
if strings.HasSuffix(m.APIBase(), "/v4") {
return m.validateV4(ctx, email, mailBoxVerify)
}
return m.validateV3(ctx, email, mailBoxVerify)
}

func (m *EmailValidatorImpl) validateV3(ctx context.Context, email string, mailBoxVerify bool) (EmailVerification, error) {
r := newHTTPRequest(m.getAddressURL("validate"))
r.setClient(m.Client())
r.addParameter("address", email)
Expand All @@ -144,13 +164,37 @@ func (m *EmailValidatorImpl) ValidateEmail(ctx context.Context, email string, ma
}
r.setBasicAuth(basicAuthUser, m.APIKey())

var response EmailVerification
err := getResponseFromJSON(ctx, r, &response)
var res EmailVerification
err := getResponseFromJSON(ctx, r, &res)
if err != nil {
return EmailVerification{}, err
}
return res, nil
}

return response, nil
func (m *EmailValidatorImpl) validateV4(ctx context.Context, email string, mailBoxVerify bool) (EmailVerification, error) {
r := newHTTPRequest(fmt.Sprintf("%s/address/validate", m.APIBase()))
r.setClient(m.Client())
r.addParameter("address", email)
if mailBoxVerify {
r.addParameter("mailbox_verification", "true")
}
r.setBasicAuth(basicAuthUser, m.APIKey())

var res v4EmailValidationResp
err := getResponseFromJSON(ctx, r, &res)
if err != nil {
return EmailVerification{}, err
}
return EmailVerification{
IsValid: res.IsValid,
MailboxVerification: res.MailboxVerification,
Parts: res.Parts,
Address: res.Address,
DidYouMean: res.DidYouMean,
IsDisposableAddress: res.IsDisposableAddress,
IsRoleAddress: res.IsRoleAddress,
Reasons: res.Reason}, nil
}

// ParseAddresses takes a list of addresses and sorts them into valid and invalid address categories.
Expand Down
31 changes: 27 additions & 4 deletions email_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import (
"github.com/mailgun/mailgun-go/v4"
)

func TestEmailValidation(t *testing.T) {
func TestEmailValidationV3(t *testing.T) {
v := mailgun.NewEmailValidator(testKey)
// API Base is set to `http://server/v3`
v.SetAPIBase(server.URL())
ctx := context.Background()

Expand All @@ -24,7 +25,29 @@ func TestEmailValidation(t *testing.T) {
ensure.True(t, ev.Parts.DisplayName == "")
ensure.DeepEqual(t, ev.Parts.LocalPart, "foo")
ensure.DeepEqual(t, ev.Parts.Domain, "mailgun.com")
ensure.True(t, ev.Reason == "")
ensure.DeepEqual(t, ev.Reason, "no-reason")
ensure.True(t, len(ev.Reasons) == 0)
}

func TestEmailValidationV4(t *testing.T) {
v := mailgun.NewEmailValidator(testKey)
// API Base is set to `http://server/v4`
v.SetAPIBase(server.URL4())
ctx := context.Background()

ev, err := v.ValidateEmail(ctx, "[email protected]", false)
ensure.Nil(t, err)

ensure.True(t, ev.IsValid)
ensure.DeepEqual(t, ev.MailboxVerification, "")
ensure.False(t, ev.IsDisposableAddress)
ensure.False(t, ev.IsRoleAddress)
ensure.True(t, ev.Parts.DisplayName == "")
ensure.DeepEqual(t, ev.Parts.LocalPart, "foo")
ensure.DeepEqual(t, ev.Parts.Domain, "mailgun.com")
ensure.DeepEqual(t, ev.Reason, "")
ensure.True(t, len(ev.Reasons) != 0)
ensure.DeepEqual(t, ev.Reasons[0], "no-reason")
}

func TestParseAddresses(t *testing.T) {
Expand Down Expand Up @@ -61,7 +84,7 @@ func TestUnmarshallResponse(t *testing.T) {
"domain": "aol.com",
"local_part": "some_email"
},
"reason": null
"reason": "no-reason"
}`)
var ev mailgun.EmailVerification
err := json.Unmarshal(payload, &ev)
Expand All @@ -74,5 +97,5 @@ func TestUnmarshallResponse(t *testing.T) {
ensure.True(t, ev.Parts.DisplayName == "")
ensure.DeepEqual(t, ev.Parts.LocalPart, "some_email")
ensure.DeepEqual(t, ev.Parts.Domain, "aol.com")
ensure.True(t, ev.Reason == "")
ensure.DeepEqual(t, ev.Reason, "no-reason")
}
Loading

0 comments on commit 6279ab3

Please sign in to comment.