From 31fbc5899f71fb330da02293751bd71916c70168 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Tue, 17 Sep 2024 20:13:58 +0300 Subject: [PATCH 01/47] refactored --- mailgun.go | 13 +++++++++---- subaccounts.go | 5 +++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/mailgun.go b/mailgun.go index 0324f8dd..0353952d 100644 --- a/mailgun.go +++ b/mailgun.go @@ -105,6 +105,7 @@ const ( mimeMessagesEndpoint = "messages.mime" bouncesEndpoint = "bounces" statsTotalEndpoint = "stats/total" + metricsEndpoint = "analytics/metrics" domainsEndpoint = "domains" tagsEndpoint = "tags" eventsEndpoint = "events" @@ -369,6 +370,14 @@ func (mg *MailgunImpl) GetCurlOutput() string { return mg.capturedCurlOutput } +type Resolution string + +const ( + ResolutionHour = Resolution("hour") + ResolutionDay = Resolution("day") + ResolutionMonth = Resolution("month") +) + // generateApiUrl renders a URL for an API endpoint using the domain and endpoint name. func generateApiUrl(m Mailgun, endpoint string) string { return fmt.Sprintf("%s/%s/%s", m.APIBase(), m.Domain(), endpoint) @@ -425,10 +434,6 @@ func generatePublicApiUrl(m Mailgun, endpoint string) string { return fmt.Sprintf("%s/%s", m.APIBase(), endpoint) } -func generateSubaccountsApiUrl(m Mailgun) string { - return fmt.Sprintf("%s/%s/%s", m.APIBase(), accountsEndpoint, subaccountsEndpoint) -} - // generateParameterizedUrl works as generateApiUrl, but supports query parameters. func generateParameterizedUrl(m Mailgun, endpoint string, payload payload) (string, error) { paramBuffer, err := payload.getPayloadBuffer() diff --git a/subaccounts.go b/subaccounts.go index b5aac2ca..75d9bb0e 100644 --- a/subaccounts.go +++ b/subaccounts.go @@ -2,6 +2,7 @@ package mailgun import ( "context" + "fmt" "strconv" ) @@ -244,3 +245,7 @@ func (mg *MailgunImpl) DisableSubaccount(ctx context.Context, subaccountId strin err := postResponseFromJSON(ctx, r, nil, &resp) return resp, err } + +func generateSubaccountsApiUrl(m Mailgun) string { + return fmt.Sprintf("%s/%s/%s", m.APIBase(), accountsEndpoint, subaccountsEndpoint) +} From 72ec3c8efa87cbc7f09cb30d31b1d209c26bfe35 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Tue, 17 Sep 2024 20:14:27 +0300 Subject: [PATCH 02/47] refactored and deprecate --- stats.go | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/stats.go b/stats.go index 04fe7605..a9c720e7 100644 --- a/stats.go +++ b/stats.go @@ -67,16 +67,6 @@ type statsTotalResponse struct { Stats []Stats `json:"stats"` } -// Used by GetStats() to specify the resolution stats are for -type Resolution string - -// Indicate which resolution a stat response for request is for -const ( - ResolutionHour = Resolution("hour") - ResolutionDay = Resolution("day") - ResolutionMonth = Resolution("month") -) - // Options for GetStats() type GetStatOptions struct { Resolution Resolution @@ -85,7 +75,8 @@ type GetStatOptions struct { End time.Time } -// GetStats returns total stats for a given domain for the specified time period +// GetStats returns total stats for a given domain for the specified time period. +// Deprecated: Use ListMetrics instead. func (mg *MailgunImpl) GetStats(ctx context.Context, events []string, opts *GetStatOptions) ([]Stats, error) { r := newHTTPRequest(generateApiUrl(mg, statsTotalEndpoint)) From 73d6a29455d0621f64e404d0d6f60674ba401921 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Tue, 17 Sep 2024 20:14:41 +0300 Subject: [PATCH 03/47] RFC2822Time --- rfc2822.go | 22 +++++++++++++--------- rfc2822_test.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 rfc2822_test.go diff --git a/rfc2822.go b/rfc2822.go index e511d75e..e02c1123 100644 --- a/rfc2822.go +++ b/rfc2822.go @@ -2,11 +2,12 @@ package mailgun import ( "strconv" - "strings" "time" + + "github.com/pkg/errors" ) -// Mailgun uses RFC2822 format for timestamps everywhere ('Thu, 13 Oct 2011 18:02:00 GMT'), but +// RFC2822Time Mailgun uses RFC2822 format for timestamps everywhere ('Thu, 13 Oct 2011 18:02:00 GMT'), but // by default Go's JSON package uses another format when decoding/encoding timestamps. type RFC2822Time time.Time @@ -35,15 +36,18 @@ func (t *RFC2822Time) UnmarshalJSON(s []byte) error { if err != nil { return err } - if *(*time.Time)(t), err = time.Parse(time.RFC1123, q); err != nil { - if strings.Contains(err.Error(), "extra text") { - if *(*time.Time)(t), err = time.Parse(time.RFC1123Z, q); err != nil { - return err - } - return nil + + var err1 error + *(*time.Time)(t), err1 = time.Parse(time.RFC1123, q) + if err1 != nil { + var err2 error + *(*time.Time)(t), err2 = time.Parse(time.RFC1123Z, q) + if err2 != nil { + // TODO(go1.20): use errors.Join: + return errors.Errorf("%s; %s", err1, err2) } - return err } + return nil } diff --git a/rfc2822_test.go b/rfc2822_test.go new file mode 100644 index 00000000..79ccdf95 --- /dev/null +++ b/rfc2822_test.go @@ -0,0 +1,49 @@ +package mailgun + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/facebookgo/ensure" +) + +func TestUnmarshalRFC2822Time(t *testing.T) { + type Req struct { + CreatedAt RFC2822Time `json:"created_at"` + } + + tests := []struct { + name string + s string + want Req + wantErr bool + }{ + { + name: "RFC1123", + s: `{"created_at":"Thu, 13 Oct 2011 18:02:00 GMT"}`, + wantErr: false, + want: Req{CreatedAt: RFC2822Time(time.Date(2011, 10, 13, 18, 2, 0, 0, time.UTC))}, + }, + { + name: "RFC1123Z", + s: `{"created_at":"Thu, 13 Oct 2011 18:02:00 +0000"}`, + wantErr: false, + want: Req{CreatedAt: RFC2822Time(time.Date(2011, 10, 13, 18, 2, 0, 0, time.UTC))}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var req Req + err := json.Unmarshal([]byte(tt.s), &req) + if (err != nil) != tt.wantErr { + t.Errorf(" error = %v, wantErr %v", err, tt.wantErr) + } + + ensure.True(t, time.Time(tt.want.CreatedAt).Equal(time.Time(req.CreatedAt)), + fmt.Sprintf("want: %s; got: %s", tt.want.CreatedAt, req.CreatedAt)) + }) + } +} From c3d2e09715a656aeb144a87520e0901c6525ea2b Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Tue, 17 Sep 2024 20:14:49 +0300 Subject: [PATCH 04/47] WIP --- analytics.go | 56 +++++++++++++++++++++++++ analytics_request.go | 40 ++++++++++++++++++ analytics_response.go | 95 +++++++++++++++++++++++++++++++++++++++++++ analytics_test.go | 23 +++++++++++ 4 files changed, 214 insertions(+) create mode 100644 analytics.go create mode 100644 analytics_request.go create mode 100644 analytics_response.go create mode 100644 analytics_test.go diff --git a/analytics.go b/analytics.go new file mode 100644 index 00000000..c8eb4181 --- /dev/null +++ b/analytics.go @@ -0,0 +1,56 @@ +package mailgun + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/pkg/errors" +) + +// ListMetrics returns account metrics. +// Must be /v1 API. +// https://documentation.mailgun.com/docs/mailgun/api-reference/openapi-final/tag/Metrics/ +func (c *Client) ListMetrics(ctx context.Context, opts MetricsOptions) (*MetricsResponse, error) { + url := fmt.Sprintf("%s", metricsEndpoint) + mRequest, err := json.Marshal(opts) + if err != nil { + return nil, errors.Wrap(err, "while marshalling analytics metrics request") + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(mRequest)) + if err != nil { + return nil, errors.Wrap(err, "while creating analytics metrics request") + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, errors.Wrap(err, "while making analytics metrics request") + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, errors.Errorf("POST on '%s' returned %s", url, string(body)) + } + + var ret MetricsResponse + if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil { + return nil, errors.Wrap(err, "while decoding analytics metrics response") + } + + return &ret, nil +} + +type MetricsPagination struct { + // Colon-separated value indicating column name and sort direction e.g. 'domain:asc'. + Sort string `json:"sort"` + // The number of items to skip over when satisfying the request. To get the first page of data set skip to zero. Then increment the skip by the limit for subsequent calls. + Skip int64 `json:"skip"` + // The maximum number of items returned in the response. + Limit int64 `json:"limit"` + // The total number of items in the query result set. + Total int64 `json:"total"` +} diff --git a/analytics_request.go b/analytics_request.go new file mode 100644 index 00000000..f4a7952f --- /dev/null +++ b/analytics_request.go @@ -0,0 +1,40 @@ +package mailgun + +type MetricsOptions struct { + // A start date (default: 7 days before current time). + Start RFC2822Time `json:"start"` + // An end date (default: current time). + End RFC2822Time `json:"end"` + // A resolution in the format of 'day' 'hour' 'month'. Default is day. + Resolution Resolution `json:"resolution,omitempty"` + // A duration in the format of '1d' '2h' '2m'. + // If duration is provided then it is calculated from the end date and overwrites the start date. + Duration string `json:"duration,omitempty"` + // Attributes of the metric data such as 'time' 'domain' 'ip' 'ip_pool' 'recipient_domain' 'tag' 'country' 'subaccount'. + Dimensions []string `json:"dimensions,omitempty"` + // Name of the metrics to receive the stats for such as 'accepted_count' 'delivered_count' 'accepted_rate'. + Metrics []string `json:"metrics,omitempty"` + // Filters to apply to the query. + Filter MetricsFilterPredicateGroup `json:"filter,omitempty"` + // Include stats from all subaccounts. + IncludeSubaccounts bool `json:"include_subaccounts,omitempty"` + // Include top-level aggregate metrics. + IncludeAggregates bool `json:"include_aggregates,omitempty"` + // Attributes used for pagination and sorting. + Pagination MetricsPagination `json:"pagination,omitempty"` +} + +type MetricsLabeledValue struct { + Label string `json:"label"` + Value string `json:"value"` +} + +type MetricsFilterPredicate struct { + Attribute string `json:"attribute"` + Comparator string `json:"comparator"` + LabeledValues []MetricsLabeledValue `json:"values,omitempty"` +} + +type MetricsFilterPredicateGroup struct { + BoolGroupAnd []MetricsFilterPredicate `json:"AND,omitempty"` +} diff --git a/analytics_response.go b/analytics_response.go new file mode 100644 index 00000000..3c2a2337 --- /dev/null +++ b/analytics_response.go @@ -0,0 +1,95 @@ +package mailgun + +type MetricsResponse struct { + Start string `json:"start"` + End string `json:"end"` + Resolution Resolution `json:"resolution"` + Duration string `json:"duration"` + Dimensions []string `json:"dimensions"` + Pagination MetricsPagination `json:"pagination"` + Items []MetricsItem `json:"items"` + Aggregates MetricsAggregates `json:"aggregates"` +} + +type MetricsItem struct { + Dimensions []MetricsDimension `json:"dimensions"` + Metrics Metrics `json:"metrics"` +} + +type MetricsAggregates struct { + Metrics Metrics `json:"metrics"` +} + +type Metrics struct { + AcceptedIncomingCount *uint64 `json:"accepted_incoming_count"` + AcceptedOutgoingCount *uint64 `json:"accepted_outgoing_count"` + AcceptedCount *uint64 `json:"accepted_count"` + DeliveredSMTPCount *uint64 `json:"delivered_smtp_count"` + DeliveredHTTPCount *uint64 `json:"delivered_http_count"` + DeliveredOptimizedCount *uint64 `json:"delivered_optimized_count"` + DeliveredCount *uint64 `json:"delivered_count"` + StoredCount *uint64 `json:"stored_count"` + ProcessedCount *uint64 `json:"processed_count"` + SentCount *uint64 `json:"sent_count"` + OpenedCount *uint64 `json:"opened_count"` + ClickedCount *uint64 `json:"clicked_count"` + UniqueOpenedCount *uint64 `json:"unique_opened_count"` + UniqueClickedCount *uint64 `json:"unique_clicked_count"` + UnsubscribedCount *uint64 `json:"unsubscribed_count"` + ComplainedCount *uint64 `json:"complained_count"` + FailedCount *uint64 `json:"failed_count"` + TemporaryFailedCount *uint64 `json:"temporary_failed_count"` + PermanentFailedCount *uint64 `json:"permanent_failed_count"` + ESPBlockCount *uint64 `json:"esp_block_count"` + WebhookCount *uint64 `json:"webhook_count"` + PermanentFailedOptimizedCount *uint64 `json:"permanent_failed_optimized_count"` + PermanentFailedOldCount *uint64 `json:"permanent_failed_old_count"` + BouncedCount *uint64 `json:"bounced_count"` + HardBouncesCount *uint64 `json:"hard_bounces_count"` + SoftBouncesCount *uint64 `json:"soft_bounces_count"` + DelayedBounceCount *uint64 `json:"delayed_bounce_count"` + SuppressedBouncesCount *uint64 `json:"suppressed_bounces_count"` + SuppressedUnsubscribedCount *uint64 `json:"suppressed_unsubscribed_count"` + SuppressedComplaintsCount *uint64 `json:"suppressed_complaints_count"` + DeliveredFirstAttemptCount *uint64 `json:"delivered_first_attempt_count"` + DelayedFirstAttemptCount *uint64 `json:"delayed_first_attempt_count"` + DeliveredSubsequentCount *uint64 `json:"delivered_subsequent_count"` + DeliveredTwoPlusAttemptsCount *uint64 `json:"delivered_two_plus_attempts_count"` + + DeliveredRate string `json:"delivered_rate"` + OpenedRate string `json:"opened_rate"` + ClickedRate string `json:"clicked_rate"` + UniqueOpenedRate string `json:"unique_opened_rate"` + UniqueClickedRate string `json:"unique_clicked_rate"` + UnsubscribedRate string `json:"unsubscribed_rate"` + ComplainedRate string `json:"complained_rate"` + BounceRate string `json:"bounce_rate"` + FailRate string `json:"fail_rate"` + PermanentFailRate string `json:"permanent_fail_rate"` + TemporaryFailRate string `json:"temporary_fail_rate"` + DelayedRate string `json:"delayed_rate"` + + // usage metrics + EmailValidationCount *uint64 `json:"email_validation_count"` + EmailValidationPublicCount *uint64 `json:"email_validation_public_count"` + EmailValidationValidCount *uint64 `json:"email_validation_valid_count"` + EmailValidationSingleCount *uint64 `json:"email_validation_single_count"` + EmailValidationBulkCount *uint64 `json:"email_validation_bulk_count"` + EmailValidationListCount *uint64 `json:"email_validation_list_count"` + EmailValidationMailgunCount *uint64 `json:"email_validation_mailgun_count"` + EmailValidationMailjetCount *uint64 `json:"email_validation_mailjet_count"` + EmailPreviewCount *uint64 `json:"email_preview_count"` + EmailPreviewFailedCount *uint64 `json:"email_preview_failed_count"` + LinkValidationCount *uint64 `json:"link_validation_count"` + LinkValidationFailedCount *uint64 `json:"link_validation_failed_count"` + SeedTestCount *uint64 `json:"seed_test_count"` +} + +type MetricsDimension struct { + // The dimension + Dimension string `json:"dimension"` + // The dimension value + Value string `json:"value"` + // The dimension value in displayable form + DisplayValue string `json:"display_value"` +} diff --git a/analytics_test.go b/analytics_test.go new file mode 100644 index 00000000..0a96c7a4 --- /dev/null +++ b/analytics_test.go @@ -0,0 +1,23 @@ +package mailgun + +var mockAcceptedIncomingCount uint64 = 10 + +var expectedResponse = MetricsResponse{ + Start: "Mon, 15 Apr 2024 00:00:00 +0000", + Dimensions: []string{"time"}, + Items: []MetricsItem{ + { + Dimensions: []MetricsDimension{ + { + Dimension: "time", + Value: "Mon, 15 Apr 2024 00:00:00 +0000", + DisplayValue: "Mon, 15 Apr 2024 00:00:00 +0000", + }, + }, + Metrics: Metrics{ + AcceptedIncomingCount: &mockAcceptedIncomingCount, + ClickedRate: "0.8300", + }, + }, + }, +} From 8136d075a79bcbdecfe017fd6009258a8c8bf2ab Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Tue, 17 Sep 2024 21:22:41 +0300 Subject: [PATCH 05/47] moved Resolution --- mailgun.go | 8 -------- reporting.go | 11 +++++++++++ 2 files changed, 11 insertions(+), 8 deletions(-) create mode 100644 reporting.go diff --git a/mailgun.go b/mailgun.go index 0353952d..15c10dc2 100644 --- a/mailgun.go +++ b/mailgun.go @@ -370,14 +370,6 @@ func (mg *MailgunImpl) GetCurlOutput() string { return mg.capturedCurlOutput } -type Resolution string - -const ( - ResolutionHour = Resolution("hour") - ResolutionDay = Resolution("day") - ResolutionMonth = Resolution("month") -) - // generateApiUrl renders a URL for an API endpoint using the domain and endpoint name. func generateApiUrl(m Mailgun, endpoint string) string { return fmt.Sprintf("%s/%s/%s", m.APIBase(), m.Domain(), endpoint) diff --git a/reporting.go b/reporting.go new file mode 100644 index 00000000..f9813839 --- /dev/null +++ b/reporting.go @@ -0,0 +1,11 @@ +package mailgun + +// common things for stats and metrics + +type Resolution string + +const ( + ResolutionHour = Resolution("hour") + ResolutionDay = Resolution("day") + ResolutionMonth = Resolution("month") +) From 9495404a6f027f9e9a5ff7dcb049e1c4be60fbc9 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Tue, 17 Sep 2024 21:26:20 +0300 Subject: [PATCH 06/47] bytes.Buffer --- analytics.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/analytics.go b/analytics.go index c8eb4181..f84fea65 100644 --- a/analytics.go +++ b/analytics.go @@ -16,11 +16,14 @@ import ( // https://documentation.mailgun.com/docs/mailgun/api-reference/openapi-final/tag/Metrics/ func (c *Client) ListMetrics(ctx context.Context, opts MetricsOptions) (*MetricsResponse, error) { url := fmt.Sprintf("%s", metricsEndpoint) - mRequest, err := json.Marshal(opts) + + var buf bytes.Buffer + err := json.NewEncoder(&buf).Encode(opts) if err != nil { return nil, errors.Wrap(err, "while marshalling analytics metrics request") } - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(mRequest)) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf) if err != nil { return nil, errors.Wrap(err, "while creating analytics metrics request") } From d4fe9a0d0fee3326594689de324e224260729cbd Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Tue, 17 Sep 2024 21:29:38 +0300 Subject: [PATCH 07/47] NOTE --- analytics.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/analytics.go b/analytics.go index f84fea65..56bb61c3 100644 --- a/analytics.go +++ b/analytics.go @@ -12,7 +12,10 @@ import ( ) // ListMetrics returns account metrics. -// Must be /v1 API. +// +// NOTE: Only for v1 API. To use the /v1 version define MG_URL in the environment variable +// as `https://api.mailgun.net/v1` or set `v.SetAPIBase("https://api.mailgun.net/v1")` +// // https://documentation.mailgun.com/docs/mailgun/api-reference/openapi-final/tag/Metrics/ func (c *Client) ListMetrics(ctx context.Context, opts MetricsOptions) (*MetricsResponse, error) { url := fmt.Sprintf("%s", metricsEndpoint) From 66a47bbad1c253ca723486042739fc192a352617 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:15:35 +0300 Subject: [PATCH 08/47] mailgun-go --- analytics.go | 38 ++++++++++---------------------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/analytics.go b/analytics.go index 56bb61c3..371e229b 100644 --- a/analytics.go +++ b/analytics.go @@ -1,12 +1,7 @@ package mailgun import ( - "bytes" "context" - "encoding/json" - "fmt" - "io" - "net/http" "github.com/pkg/errors" ) @@ -17,34 +12,21 @@ import ( // as `https://api.mailgun.net/v1` or set `v.SetAPIBase("https://api.mailgun.net/v1")` // // https://documentation.mailgun.com/docs/mailgun/api-reference/openapi-final/tag/Metrics/ -func (c *Client) ListMetrics(ctx context.Context, opts MetricsOptions) (*MetricsResponse, error) { - url := fmt.Sprintf("%s", metricsEndpoint) +func (mg *MailgunImpl) ListMetrics(ctx context.Context, opts MetricsOptions) (*MetricsResponse, error) { + payload := newJSONEncodedPayload(opts) + req := newHTTPRequest(generatePublicApiUrl(mg, metricsEndpoint)) + req.setClient(mg.Client()) + req.setBasicAuth(basicAuthUser, mg.APIKey()) - var buf bytes.Buffer - err := json.NewEncoder(&buf).Encode(opts) + resp, err := makePostRequest(ctx, req, payload) if err != nil { - return nil, errors.Wrap(err, "while marshalling analytics metrics request") - } - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf) - if err != nil { - return nil, errors.Wrap(err, "while creating analytics metrics request") - } - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, errors.Wrap(err, "while making analytics metrics request") - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return nil, errors.Errorf("POST on '%s' returned %s", url, string(body)) + return nil, errors.Errorf("POST %s failed: %s", metricsEndpoint, err) } var ret MetricsResponse - if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil { - return nil, errors.Wrap(err, "while decoding analytics metrics response") + err = resp.parseFromJSON(&ret) + if err != nil { + return nil, errors.Wrap(err, "decoding response") } return &ret, nil From cd7366587c8092c0b076946f0211bdcce0accd17 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:17:55 +0300 Subject: [PATCH 09/47] check v1 --- analytics.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/analytics.go b/analytics.go index 371e229b..dd4be8de 100644 --- a/analytics.go +++ b/analytics.go @@ -2,6 +2,7 @@ package mailgun import ( "context" + "strings" "github.com/pkg/errors" ) @@ -13,6 +14,10 @@ import ( // // https://documentation.mailgun.com/docs/mailgun/api-reference/openapi-final/tag/Metrics/ func (mg *MailgunImpl) ListMetrics(ctx context.Context, opts MetricsOptions) (*MetricsResponse, error) { + if !strings.HasSuffix(mg.APIBase(), "/v1") { + return nil, errors.New("only v1 is supported") + } + payload := newJSONEncodedPayload(opts) req := newHTTPRequest(generatePublicApiUrl(mg, metricsEndpoint)) req.setClient(mg.Client()) From b7456ba95a0124b23890921b4b004dd0cbbfc6c3 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:18:11 +0300 Subject: [PATCH 10/47] TODO --- analytics.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/analytics.go b/analytics.go index dd4be8de..3676c2f3 100644 --- a/analytics.go +++ b/analytics.go @@ -18,6 +18,8 @@ func (mg *MailgunImpl) ListMetrics(ctx context.Context, opts MetricsOptions) (*M return nil, errors.New("only v1 is supported") } + // TODO: set mg.domain? + payload := newJSONEncodedPayload(opts) req := newHTTPRequest(generatePublicApiUrl(mg, metricsEndpoint)) req.setClient(mg.Client()) From 907cd2623e17c92b1c0e5d19a5cdf53096da5a9f Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:30:43 +0300 Subject: [PATCH 11/47] filter by domain --- analytics.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/analytics.go b/analytics.go index 3676c2f3..531532df 100644 --- a/analytics.go +++ b/analytics.go @@ -7,7 +7,7 @@ import ( "github.com/pkg/errors" ) -// ListMetrics returns account metrics. +// ListMetrics returns domain/account metrics. // // NOTE: Only for v1 API. To use the /v1 version define MG_URL in the environment variable // as `https://api.mailgun.net/v1` or set `v.SetAPIBase("https://api.mailgun.net/v1")` @@ -15,10 +15,19 @@ import ( // https://documentation.mailgun.com/docs/mailgun/api-reference/openapi-final/tag/Metrics/ func (mg *MailgunImpl) ListMetrics(ctx context.Context, opts MetricsOptions) (*MetricsResponse, error) { if !strings.HasSuffix(mg.APIBase(), "/v1") { - return nil, errors.New("only v1 is supported") + return nil, errors.New("only v1 API is supported") } - // TODO: set mg.domain? + domain := mg.Domain() + if domain != "" { + domainFilter := MetricsFilterPredicate{ + Attribute: "domain", + Comparator: "=", + LabeledValues: []MetricsLabeledValue{{Label: domain, Value: domain}}, + } + + opts.Filter.BoolGroupAnd = append(opts.Filter.BoolGroupAnd, domainFilter) + } payload := newJSONEncodedPayload(opts) req := newHTTPRequest(generatePublicApiUrl(mg, metricsEndpoint)) From 7a92b35bfbe04d6e9c7a918fea188f09a634414b Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:32:04 +0300 Subject: [PATCH 12/47] removed redundant --- stats.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stats.go b/stats.go index a9c720e7..533c13d9 100644 --- a/stats.go +++ b/stats.go @@ -106,7 +106,7 @@ func (mg *MailgunImpl) GetStats(ctx context.Context, events []string, opts *GetS err := getResponseFromJSON(ctx, r, &res) if err != nil { return nil, err - } else { - return res.Stats, nil } + + return res.Stats, nil } From 1b6357492a93d90c225c787b5eb96281db2ca286 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:33:36 +0300 Subject: [PATCH 13/47] fmt --- analytics.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/analytics.go b/analytics.go index 531532df..a4b23892 100644 --- a/analytics.go +++ b/analytics.go @@ -29,11 +29,12 @@ func (mg *MailgunImpl) ListMetrics(ctx context.Context, opts MetricsOptions) (*M opts.Filter.BoolGroupAnd = append(opts.Filter.BoolGroupAnd, domainFilter) } - payload := newJSONEncodedPayload(opts) req := newHTTPRequest(generatePublicApiUrl(mg, metricsEndpoint)) req.setClient(mg.Client()) req.setBasicAuth(basicAuthUser, mg.APIKey()) + payload := newJSONEncodedPayload(opts) + resp, err := makePostRequest(ctx, req, payload) if err != nil { return nil, errors.Errorf("POST %s failed: %s", metricsEndpoint, err) From e5fa39cfa5666550b0807e42131128331272aa81 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:35:06 +0300 Subject: [PATCH 14/47] typo --- analytics.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analytics.go b/analytics.go index a4b23892..683fd831 100644 --- a/analytics.go +++ b/analytics.go @@ -10,7 +10,7 @@ import ( // ListMetrics returns domain/account metrics. // // NOTE: Only for v1 API. To use the /v1 version define MG_URL in the environment variable -// as `https://api.mailgun.net/v1` or set `v.SetAPIBase("https://api.mailgun.net/v1")` +// as `https://api.mailgun.net/v1` or set `mg.SetAPIBase("https://api.mailgun.net/v1")` // // https://documentation.mailgun.com/docs/mailgun/api-reference/openapi-final/tag/Metrics/ func (mg *MailgunImpl) ListMetrics(ctx context.Context, opts MetricsOptions) (*MetricsResponse, error) { From bd98c8f26dadb0854e01b0587c0a9ad25a41dc1b Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sat, 19 Oct 2024 17:14:14 +0300 Subject: [PATCH 15/47] sort --- analytics_response.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/analytics_response.go b/analytics_response.go index 3c2a2337..ff5a2b48 100644 --- a/analytics_response.go +++ b/analytics_response.go @@ -6,9 +6,9 @@ type MetricsResponse struct { Resolution Resolution `json:"resolution"` Duration string `json:"duration"` Dimensions []string `json:"dimensions"` - Pagination MetricsPagination `json:"pagination"` - Items []MetricsItem `json:"items"` Aggregates MetricsAggregates `json:"aggregates"` + Items []MetricsItem `json:"items"` + Pagination MetricsPagination `json:"pagination"` } type MetricsItem struct { From 4ded58f34073490f041c7858053c09670b1061c5 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sat, 19 Oct 2024 18:02:57 +0300 Subject: [PATCH 16/47] added iterator --- analytics.go | 76 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 61 insertions(+), 15 deletions(-) diff --git a/analytics.go b/analytics.go index 683fd831..8c64f749 100644 --- a/analytics.go +++ b/analytics.go @@ -13,7 +13,7 @@ import ( // as `https://api.mailgun.net/v1` or set `mg.SetAPIBase("https://api.mailgun.net/v1")` // // https://documentation.mailgun.com/docs/mailgun/api-reference/openapi-final/tag/Metrics/ -func (mg *MailgunImpl) ListMetrics(ctx context.Context, opts MetricsOptions) (*MetricsResponse, error) { +func (mg *MailgunImpl) ListMetrics(opts MetricsOptions) (*MetricsIterator, error) { if !strings.HasSuffix(mg.APIBase(), "/v1") { return nil, errors.New("only v1 API is supported") } @@ -29,24 +29,18 @@ func (mg *MailgunImpl) ListMetrics(ctx context.Context, opts MetricsOptions) (*M opts.Filter.BoolGroupAnd = append(opts.Filter.BoolGroupAnd, domainFilter) } + if opts.Pagination.Limit == 0 { + opts.Pagination.Limit = 10 + } + req := newHTTPRequest(generatePublicApiUrl(mg, metricsEndpoint)) req.setClient(mg.Client()) req.setBasicAuth(basicAuthUser, mg.APIKey()) - payload := newJSONEncodedPayload(opts) - - resp, err := makePostRequest(ctx, req, payload) - if err != nil { - return nil, errors.Errorf("POST %s failed: %s", metricsEndpoint, err) - } - - var ret MetricsResponse - err = resp.parseFromJSON(&ret) - if err != nil { - return nil, errors.Wrap(err, "decoding response") - } - - return &ret, nil + return &MetricsIterator{ + opts: opts, + req: req, + }, nil } type MetricsPagination struct { @@ -59,3 +53,55 @@ type MetricsPagination struct { // The total number of items in the query result set. Total int64 `json:"total"` } + +type MetricsIterator struct { + opts MetricsOptions + req *httpRequest + resp MetricsResponse + err error +} + +func (iter *MetricsIterator) Err() error { + return iter.err +} + +// Next retrieves the next page of items from the api. Returns false when there are +// no more pages to retrieve or if there was an error. +// Use `.Err()` to retrieve the error +func (iter *MetricsIterator) Next(ctx context.Context, resp *MetricsResponse) bool { + if iter.err != nil { + return false + } + + iter.err = iter.fetch(ctx) + if iter.err != nil { + return false + } + + *resp = iter.resp + if len(iter.resp.Items) == 0 { + return false + } + iter.opts.Pagination.Skip = iter.opts.Pagination.Skip + iter.opts.Pagination.Limit + + return true +} + +func (iter *MetricsIterator) fetch(ctx context.Context) error { + payload := newJSONEncodedPayload(iter.opts) + + httpResp, err := makePostRequest(ctx, iter.req, payload) + if err != nil { + return errors.Errorf("POST %s failed: %s", metricsEndpoint, err) + } + + var resp MetricsResponse + err = httpResp.parseFromJSON(&resp) + if err != nil { + return errors.Wrap(err, "decoding response") + } + + iter.resp = resp + + return nil +} From dbe1bc280d13ea1cc16fede4428ebde38c804184 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sat, 19 Oct 2024 18:30:03 +0300 Subject: [PATCH 17/47] env --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c64a9442..4daa3427 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store .idea/ cmd/mailgun/mailgun +/.env From 555f55ef2b87ab3514f9baf86e91686f60995ed9 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sat, 19 Oct 2024 18:30:18 +0300 Subject: [PATCH 18/47] allow v1 for https://documentation.mailgun.com/docs/mailgun/api-reference/openapi-final/tag/Metrics/ --- httphelpers.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httphelpers.go b/httphelpers.go index b88f820d..37b5834b 100644 --- a/httphelpers.go +++ b/httphelpers.go @@ -18,7 +18,7 @@ import ( "github.com/pkg/errors" ) -var validURL = regexp.MustCompile(`/v[2-5].*`) +var validURL = regexp.MustCompile(`/v[1-5].*`) type httpRequest struct { URL string @@ -332,7 +332,7 @@ func (r *httpRequest) generateUrlWithParameters() (string, error) { } if !validURL.MatchString(url.Path) { - return "", errors.New(`BaseAPI must end with a /v2, /v3 or /v4; setBaseAPI("https://host/v3")`) + return "", errors.New(`BaseAPI must end with a /v1, /v2, /v3 or /v4; setBaseAPI("https://host/v3")`) } q := url.Query() From 09036956916f80f4e5049f90ca30c2b526a79248 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sat, 19 Oct 2024 18:35:16 +0300 Subject: [PATCH 19/47] ExampleMailgunImpl_ListMetrics --- analytics_request.go | 1 + 1 file changed, 1 insertion(+) diff --git a/analytics_request.go b/analytics_request.go index f4a7952f..b7add477 100644 --- a/analytics_request.go +++ b/analytics_request.go @@ -2,6 +2,7 @@ package mailgun type MetricsOptions struct { // A start date (default: 7 days before current time). + // TODO: 400: {"message":"Invalid format for parameter start: Thu, 19 Sep 2024 18:32:12 EEST"} Start RFC2822Time `json:"start"` // An end date (default: current time). End RFC2822Time `json:"end"` From c5e99db9a348e93c7fc2e322766975a7bb648831 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sat, 19 Oct 2024 18:35:24 +0300 Subject: [PATCH 20/47] ExampleMailgunImpl_ListMetrics --- examples_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/examples_test.go b/examples_test.go index 3081538e..660552ab 100644 --- a/examples_test.go +++ b/examples_test.go @@ -191,3 +191,44 @@ func ExampleMailgunImpl_VerifyWebhookSignature() { os.Exit(1) } } + +func ExampleMailgunImpl_ListMetrics() { + mg, err := mailgun.NewMailgunFromEnv() + if err != nil { + log.Fatal(err) + } + + opts := mailgun.MetricsOptions{ + Start: mailgun.RFC2822Time(time.Now().Add(-time.Hour * 24 * 30)), + Pagination: mailgun.MetricsPagination{ + Limit: 10, + }, + } + + iter, err := mg.ListMetrics(opts) + if err != nil { + log.Fatal(err) + } + + for i := 0; i < 2; i++ { + func() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + resp := mailgun.MetricsResponse{} + more := iter.Next(ctx, &resp) + if iter.Err() != nil { + log.Fatal(iter.Err()) + } + + b, _ := json.Marshal(resp) + log.Printf("%s", b) + + if !more { + log.Print("no more pages") + os.Exit(0) + } + }() + } + // Output: +} From b401765ae203df7bd3fe7204c09547c0630919e6 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sat, 19 Oct 2024 18:42:12 +0300 Subject: [PATCH 21/47] better error --- analytics.go | 2 +- rest_shim.go | 23 +++++++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/analytics.go b/analytics.go index 8c64f749..ca83b1ac 100644 --- a/analytics.go +++ b/analytics.go @@ -92,7 +92,7 @@ func (iter *MetricsIterator) fetch(ctx context.Context) error { httpResp, err := makePostRequest(ctx, iter.req, payload) if err != nil { - return errors.Errorf("POST %s failed: %s", metricsEndpoint, err) + return err } var resp MetricsResponse diff --git a/rest_shim.go b/rest_shim.go index 2b6def48..8837257d 100644 --- a/rest_shim.go +++ b/rest_shim.go @@ -3,6 +3,7 @@ package mailgun import ( "context" "fmt" + "net/http" ) // The MailgunGoUserAgent identifies the client to the server, for logging purposes. @@ -16,6 +17,7 @@ const MailgunGoUserAgent = "mailgun-go/" + Version type UnexpectedResponseError struct { Expected []int Actual int + Method string URL string Data []byte } @@ -23,7 +25,8 @@ type UnexpectedResponseError struct { // String() converts the error into a human-readable, logfmt-compliant string. // See http://godoc.org/github.com/kr/logfmt for details on logfmt formatting. func (e *UnexpectedResponseError) String() string { - return fmt.Sprintf("UnexpectedResponseError URL=%s ExpectedOneOf=%#v Got=%d Error: %s", e.URL, e.Expected, e.Actual, string(e.Data)) + return fmt.Sprintf("UnexpectedResponseError Method=%s URL=%s ExpectedOneOf=%#v Got=%d Error: %s", + e.Method, e.URL, e.Expected, e.Actual, string(e.Data)) } // Error() performs as String(). @@ -32,7 +35,7 @@ func (e *UnexpectedResponseError) Error() string { } // newError creates a new error condition to be returned. -func newError(url string, expected []int, got *httpResponse) error { +func newError(method, url string, expected []int, got *httpResponse) error { return &UnexpectedResponseError{ URL: url, Expected: expected, @@ -62,7 +65,7 @@ func makeRequest(ctx context.Context, r *httpRequest, method string, p payload) r.addHeader("User-Agent", MailgunGoUserAgent) rsp, err := r.makeRequest(ctx, method, p) if (err == nil) && notGood(rsp.Code, expected) { - return rsp, newError(r.URL, expected, rsp) + return rsp, newError(method, r.URL, expected, rsp) } return rsp, err } @@ -76,7 +79,7 @@ func getResponseFromJSON(ctx context.Context, r *httpRequest, v interface{}) err return err } if notGood(response.Code, expected) { - return newError(r.URL, expected, response) + return newError(http.MethodGet, r.URL, expected, response) } return response.parseFromJSON(v) } @@ -90,7 +93,7 @@ func postResponseFromJSON(ctx context.Context, r *httpRequest, p payload, v inte return err } if notGood(response.Code, expected) { - return newError(r.URL, expected, response) + return newError(http.MethodPost, r.URL, expected, response) } return response.parseFromJSON(v) } @@ -104,7 +107,7 @@ func putResponseFromJSON(ctx context.Context, r *httpRequest, p payload, v inter return err } if notGood(response.Code, expected) { - return newError(r.URL, expected, response) + return newError(http.MethodPut, r.URL, expected, response) } return response.parseFromJSON(v) } @@ -115,7 +118,7 @@ func makeGetRequest(ctx context.Context, r *httpRequest) (*httpResponse, error) r.addHeader("User-Agent", MailgunGoUserAgent) rsp, err := r.makeGetRequest(ctx) if (err == nil) && notGood(rsp.Code, expected) { - return rsp, newError(r.URL, expected, rsp) + return rsp, newError(http.MethodGet, r.URL, expected, rsp) } return rsp, err } @@ -126,7 +129,7 @@ func makePostRequest(ctx context.Context, r *httpRequest, p payload) (*httpRespo r.addHeader("User-Agent", MailgunGoUserAgent) rsp, err := r.makePostRequest(ctx, p) if (err == nil) && notGood(rsp.Code, expected) { - return rsp, newError(r.URL, expected, rsp) + return rsp, newError(http.MethodPost, r.URL, expected, rsp) } return rsp, err } @@ -137,7 +140,7 @@ func makePutRequest(ctx context.Context, r *httpRequest, p payload) (*httpRespon r.addHeader("User-Agent", MailgunGoUserAgent) rsp, err := r.makePutRequest(ctx, p) if (err == nil) && notGood(rsp.Code, expected) { - return rsp, newError(r.URL, expected, rsp) + return rsp, newError(http.MethodPut, r.URL, expected, rsp) } return rsp, err } @@ -148,7 +151,7 @@ func makeDeleteRequest(ctx context.Context, r *httpRequest) (*httpResponse, erro r.addHeader("User-Agent", MailgunGoUserAgent) rsp, err := r.makeDeleteRequest(ctx) if (err == nil) && notGood(rsp.Code, expected) { - return rsp, newError(r.URL, expected, rsp) + return rsp, newError(http.MethodDelete, r.URL, expected, rsp) } return rsp, err } From a749b31b083a800282d6420beff445d63c2d4ba4 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sat, 19 Oct 2024 18:56:19 +0300 Subject: [PATCH 22/47] UTC --- examples_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_test.go b/examples_test.go index 660552ab..1daf1b08 100644 --- a/examples_test.go +++ b/examples_test.go @@ -199,7 +199,7 @@ func ExampleMailgunImpl_ListMetrics() { } opts := mailgun.MetricsOptions{ - Start: mailgun.RFC2822Time(time.Now().Add(-time.Hour * 24 * 30)), + Start: mailgun.RFC2822Time(time.Now().UTC().Add(-time.Hour * 24 * 30)), Pagination: mailgun.MetricsPagination{ Limit: 10, }, From 3beb189e87ecbd8b727d07d2f58d16c261e046b6 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sat, 19 Oct 2024 20:32:20 +0300 Subject: [PATCH 23/47] switched RFC2822Time to RFC1123Z --- rfc2822.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfc2822.go b/rfc2822.go index e02c1123..d151a510 100644 --- a/rfc2822.go +++ b/rfc2822.go @@ -28,7 +28,7 @@ func (t RFC2822Time) IsZero() bool { } func (t RFC2822Time) MarshalJSON() ([]byte, error) { - return []byte(strconv.Quote(time.Time(t).Format(time.RFC1123))), nil + return []byte(strconv.Quote(time.Time(t).Format(time.RFC1123Z))), nil } func (t *RFC2822Time) UnmarshalJSON(s []byte) error { From 584de81fc49fdf376be3068d7b88af3b27da806f Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sat, 19 Oct 2024 20:34:56 +0300 Subject: [PATCH 24/47] end --- examples_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples_test.go b/examples_test.go index 1daf1b08..0dd17b99 100644 --- a/examples_test.go +++ b/examples_test.go @@ -200,6 +200,7 @@ func ExampleMailgunImpl_ListMetrics() { opts := mailgun.MetricsOptions{ Start: mailgun.RFC2822Time(time.Now().UTC().Add(-time.Hour * 24 * 30)), + End: mailgun.RFC2822Time(time.Now().UTC()), Pagination: mailgun.MetricsPagination{ Limit: 10, }, @@ -221,7 +222,7 @@ func ExampleMailgunImpl_ListMetrics() { log.Fatal(iter.Err()) } - b, _ := json.Marshal(resp) + b, _ := json.Marshal(resp.Items) log.Printf("%s", b) if !more { From 4d131c29529aa33560c57d93552bdd8d4d234646 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sat, 19 Oct 2024 20:36:54 +0300 Subject: [PATCH 25/47] omitempty --- analytics_response.go | 118 +++++++++++++++++++++--------------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/analytics_response.go b/analytics_response.go index ff5a2b48..96ea1436 100644 --- a/analytics_response.go +++ b/analytics_response.go @@ -21,68 +21,68 @@ type MetricsAggregates struct { } type Metrics struct { - AcceptedIncomingCount *uint64 `json:"accepted_incoming_count"` - AcceptedOutgoingCount *uint64 `json:"accepted_outgoing_count"` - AcceptedCount *uint64 `json:"accepted_count"` - DeliveredSMTPCount *uint64 `json:"delivered_smtp_count"` - DeliveredHTTPCount *uint64 `json:"delivered_http_count"` - DeliveredOptimizedCount *uint64 `json:"delivered_optimized_count"` - DeliveredCount *uint64 `json:"delivered_count"` - StoredCount *uint64 `json:"stored_count"` - ProcessedCount *uint64 `json:"processed_count"` - SentCount *uint64 `json:"sent_count"` - OpenedCount *uint64 `json:"opened_count"` - ClickedCount *uint64 `json:"clicked_count"` - UniqueOpenedCount *uint64 `json:"unique_opened_count"` - UniqueClickedCount *uint64 `json:"unique_clicked_count"` - UnsubscribedCount *uint64 `json:"unsubscribed_count"` - ComplainedCount *uint64 `json:"complained_count"` - FailedCount *uint64 `json:"failed_count"` - TemporaryFailedCount *uint64 `json:"temporary_failed_count"` - PermanentFailedCount *uint64 `json:"permanent_failed_count"` - ESPBlockCount *uint64 `json:"esp_block_count"` - WebhookCount *uint64 `json:"webhook_count"` - PermanentFailedOptimizedCount *uint64 `json:"permanent_failed_optimized_count"` - PermanentFailedOldCount *uint64 `json:"permanent_failed_old_count"` - BouncedCount *uint64 `json:"bounced_count"` - HardBouncesCount *uint64 `json:"hard_bounces_count"` - SoftBouncesCount *uint64 `json:"soft_bounces_count"` - DelayedBounceCount *uint64 `json:"delayed_bounce_count"` - SuppressedBouncesCount *uint64 `json:"suppressed_bounces_count"` - SuppressedUnsubscribedCount *uint64 `json:"suppressed_unsubscribed_count"` - SuppressedComplaintsCount *uint64 `json:"suppressed_complaints_count"` - DeliveredFirstAttemptCount *uint64 `json:"delivered_first_attempt_count"` - DelayedFirstAttemptCount *uint64 `json:"delayed_first_attempt_count"` - DeliveredSubsequentCount *uint64 `json:"delivered_subsequent_count"` - DeliveredTwoPlusAttemptsCount *uint64 `json:"delivered_two_plus_attempts_count"` + AcceptedIncomingCount *uint64 `json:"accepted_incoming_count,omitempty"` + AcceptedOutgoingCount *uint64 `json:"accepted_outgoing_count,omitempty"` + AcceptedCount *uint64 `json:"accepted_count,omitempty"` + DeliveredSMTPCount *uint64 `json:"delivered_smtp_count,omitempty"` + DeliveredHTTPCount *uint64 `json:"delivered_http_count,omitempty"` + DeliveredOptimizedCount *uint64 `json:"delivered_optimized_count,omitempty"` + DeliveredCount *uint64 `json:"delivered_count,omitempty"` + StoredCount *uint64 `json:"stored_count,omitempty"` + ProcessedCount *uint64 `json:"processed_count,omitempty"` + SentCount *uint64 `json:"sent_count,omitempty"` + OpenedCount *uint64 `json:"opened_count,omitempty"` + ClickedCount *uint64 `json:"clicked_count,omitempty"` + UniqueOpenedCount *uint64 `json:"unique_opened_count,omitempty"` + UniqueClickedCount *uint64 `json:"unique_clicked_count,omitempty"` + UnsubscribedCount *uint64 `json:"unsubscribed_count,omitempty"` + ComplainedCount *uint64 `json:"complained_count,omitempty"` + FailedCount *uint64 `json:"failed_count,omitempty"` + TemporaryFailedCount *uint64 `json:"temporary_failed_count,omitempty"` + PermanentFailedCount *uint64 `json:"permanent_failed_count,omitempty"` + ESPBlockCount *uint64 `json:"esp_block_count,omitempty"` + WebhookCount *uint64 `json:"webhook_count,omitempty"` + PermanentFailedOptimizedCount *uint64 `json:"permanent_failed_optimized_count,omitempty"` + PermanentFailedOldCount *uint64 `json:"permanent_failed_old_count,omitempty"` + BouncedCount *uint64 `json:"bounced_count,omitempty"` + HardBouncesCount *uint64 `json:"hard_bounces_count,omitempty"` + SoftBouncesCount *uint64 `json:"soft_bounces_count,omitempty"` + DelayedBounceCount *uint64 `json:"delayed_bounce_count,omitempty"` + SuppressedBouncesCount *uint64 `json:"suppressed_bounces_count,omitempty"` + SuppressedUnsubscribedCount *uint64 `json:"suppressed_unsubscribed_count,omitempty"` + SuppressedComplaintsCount *uint64 `json:"suppressed_complaints_count,omitempty"` + DeliveredFirstAttemptCount *uint64 `json:"delivered_first_attempt_count,omitempty"` + DelayedFirstAttemptCount *uint64 `json:"delayed_first_attempt_count,omitempty"` + DeliveredSubsequentCount *uint64 `json:"delivered_subsequent_count,omitempty"` + DeliveredTwoPlusAttemptsCount *uint64 `json:"delivered_two_plus_attempts_count,omitempty"` - DeliveredRate string `json:"delivered_rate"` - OpenedRate string `json:"opened_rate"` - ClickedRate string `json:"clicked_rate"` - UniqueOpenedRate string `json:"unique_opened_rate"` - UniqueClickedRate string `json:"unique_clicked_rate"` - UnsubscribedRate string `json:"unsubscribed_rate"` - ComplainedRate string `json:"complained_rate"` - BounceRate string `json:"bounce_rate"` - FailRate string `json:"fail_rate"` - PermanentFailRate string `json:"permanent_fail_rate"` - TemporaryFailRate string `json:"temporary_fail_rate"` - DelayedRate string `json:"delayed_rate"` + DeliveredRate string `json:"delivered_rate,omitempty"` + OpenedRate string `json:"opened_rate,omitempty"` + ClickedRate string `json:"clicked_rate,omitempty"` + UniqueOpenedRate string `json:"unique_opened_rate,omitempty"` + UniqueClickedRate string `json:"unique_clicked_rate,omitempty"` + UnsubscribedRate string `json:"unsubscribed_rate,omitempty"` + ComplainedRate string `json:"complained_rate,omitempty"` + BounceRate string `json:"bounce_rate,omitempty"` + FailRate string `json:"fail_rate,omitempty"` + PermanentFailRate string `json:"permanent_fail_rate,omitempty"` + TemporaryFailRate string `json:"temporary_fail_rate,omitempty"` + DelayedRate string `json:"delayed_rate,omitempty"` // usage metrics - EmailValidationCount *uint64 `json:"email_validation_count"` - EmailValidationPublicCount *uint64 `json:"email_validation_public_count"` - EmailValidationValidCount *uint64 `json:"email_validation_valid_count"` - EmailValidationSingleCount *uint64 `json:"email_validation_single_count"` - EmailValidationBulkCount *uint64 `json:"email_validation_bulk_count"` - EmailValidationListCount *uint64 `json:"email_validation_list_count"` - EmailValidationMailgunCount *uint64 `json:"email_validation_mailgun_count"` - EmailValidationMailjetCount *uint64 `json:"email_validation_mailjet_count"` - EmailPreviewCount *uint64 `json:"email_preview_count"` - EmailPreviewFailedCount *uint64 `json:"email_preview_failed_count"` - LinkValidationCount *uint64 `json:"link_validation_count"` - LinkValidationFailedCount *uint64 `json:"link_validation_failed_count"` - SeedTestCount *uint64 `json:"seed_test_count"` + EmailValidationCount *uint64 `json:"email_validation_count,omitempty"` + EmailValidationPublicCount *uint64 `json:"email_validation_public_count,omitempty"` + EmailValidationValidCount *uint64 `json:"email_validation_valid_count,omitempty"` + EmailValidationSingleCount *uint64 `json:"email_validation_single_count,omitempty"` + EmailValidationBulkCount *uint64 `json:"email_validation_bulk_count,omitempty"` + EmailValidationListCount *uint64 `json:"email_validation_list_count,omitempty"` + EmailValidationMailgunCount *uint64 `json:"email_validation_mailgun_count,omitempty"` + EmailValidationMailjetCount *uint64 `json:"email_validation_mailjet_count,omitempty"` + EmailPreviewCount *uint64 `json:"email_preview_count,omitempty"` + EmailPreviewFailedCount *uint64 `json:"email_preview_failed_count,omitempty"` + LinkValidationCount *uint64 `json:"link_validation_count,omitempty"` + LinkValidationFailedCount *uint64 `json:"link_validation_failed_count,omitempty"` + SeedTestCount *uint64 `json:"seed_test_count,omitempty"` } type MetricsDimension struct { From d864c01747c321085326f0fec5c1ab599a298327 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sat, 19 Oct 2024 20:42:32 +0300 Subject: [PATCH 26/47] for --- examples_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples_test.go b/examples_test.go index 0dd17b99..8441f2ed 100644 --- a/examples_test.go +++ b/examples_test.go @@ -222,8 +222,11 @@ func ExampleMailgunImpl_ListMetrics() { log.Fatal(iter.Err()) } - b, _ := json.Marshal(resp.Items) - log.Printf("%s", b) + fmt.Printf("Page %d:\n", i+1) + for _, item := range resp.Items { + b, _ := json.Marshal(item) + fmt.Printf("%s\n", b) + } if !more { log.Print("no more pages") @@ -231,5 +234,4 @@ func ExampleMailgunImpl_ListMetrics() { } }() } - // Output: } From 8fef31fd497f7aa625ab3dff1b7fca6a9fdb73aa Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sat, 19 Oct 2024 20:42:38 +0300 Subject: [PATCH 27/47] removed TODO --- analytics_request.go | 1 - 1 file changed, 1 deletion(-) diff --git a/analytics_request.go b/analytics_request.go index b7add477..f4a7952f 100644 --- a/analytics_request.go +++ b/analytics_request.go @@ -2,7 +2,6 @@ package mailgun type MetricsOptions struct { // A start date (default: 7 days before current time). - // TODO: 400: {"message":"Invalid format for parameter start: Thu, 19 Sep 2024 18:32:12 EEST"} Start RFC2822Time `json:"start"` // An end date (default: current time). End RFC2822Time `json:"end"` From 4e85bfe49c87c7f165111f65bd2c9ce15064d2f0 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sat, 19 Oct 2024 20:44:12 +0300 Subject: [PATCH 28/47] omitempty --- analytics_request.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/analytics_request.go b/analytics_request.go index f4a7952f..470f30b8 100644 --- a/analytics_request.go +++ b/analytics_request.go @@ -2,9 +2,9 @@ package mailgun type MetricsOptions struct { // A start date (default: 7 days before current time). - Start RFC2822Time `json:"start"` + Start RFC2822Time `json:"start,omitempty"` // An end date (default: current time). - End RFC2822Time `json:"end"` + End RFC2822Time `json:"end,omitempty"` // A resolution in the format of 'day' 'hour' 'month'. Default is day. Resolution Resolution `json:"resolution,omitempty"` // A duration in the format of '1d' '2h' '2m'. From 697621de2da78aad878b44b278d32effb1eb25da Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sat, 19 Oct 2024 20:44:16 +0300 Subject: [PATCH 29/47] Duration --- examples_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples_test.go b/examples_test.go index 8441f2ed..4ceda703 100644 --- a/examples_test.go +++ b/examples_test.go @@ -199,8 +199,8 @@ func ExampleMailgunImpl_ListMetrics() { } opts := mailgun.MetricsOptions{ - Start: mailgun.RFC2822Time(time.Now().UTC().Add(-time.Hour * 24 * 30)), - End: mailgun.RFC2822Time(time.Now().UTC()), + End: mailgun.RFC2822Time(time.Now().UTC()), + Duration: "30d", Pagination: mailgun.MetricsPagination{ Limit: 10, }, @@ -234,4 +234,5 @@ func ExampleMailgunImpl_ListMetrics() { } }() } + // Output: } From 57d40493688d7a70872059a301de14d5e84cdce4 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sat, 19 Oct 2024 20:45:37 +0300 Subject: [PATCH 30/47] link to docs --- rfc2822.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rfc2822.go b/rfc2822.go index d151a510..5f91e999 100644 --- a/rfc2822.go +++ b/rfc2822.go @@ -7,8 +7,9 @@ import ( "github.com/pkg/errors" ) -// RFC2822Time Mailgun uses RFC2822 format for timestamps everywhere ('Thu, 13 Oct 2011 18:02:00 GMT'), but +// RFC2822Time Mailgun uses RFC2822 format for timestamps everywhere ('Thu, 13 Oct 2011 18:02:00 +0000'), but // by default Go's JSON package uses another format when decoding/encoding timestamps. +// https://documentation.mailgun.com/docs/mailgun/user-manual/get-started/#date-format type RFC2822Time time.Time func NewRFC2822Time(str string) (RFC2822Time, error) { From 7f45c9661b485c14373cf16fa83600626148d576 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sat, 19 Oct 2024 20:54:09 +0300 Subject: [PATCH 31/47] get rid of github.com/pkg/errors again --- analytics.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analytics.go b/analytics.go index ca83b1ac..bc233c9d 100644 --- a/analytics.go +++ b/analytics.go @@ -4,7 +4,7 @@ import ( "context" "strings" - "github.com/pkg/errors" + "github.com/mailgun/errors" ) // ListMetrics returns domain/account metrics. From 6a9ff1abef06924767f8c4b6c7721921582debdc Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sat, 19 Oct 2024 20:59:52 +0300 Subject: [PATCH 32/47] fixed TestInvalidBaseAPI --- mailgun_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mailgun_test.go b/mailgun_test.go index 8ac3f705..815b4067 100644 --- a/mailgun_test.go +++ b/mailgun_test.go @@ -34,7 +34,7 @@ func TestInvalidBaseAPI(t *testing.T) { ctx := context.Background() _, err := mg.GetDomain(ctx, "unknown.domain") ensure.NotNil(t, err) - ensure.DeepEqual(t, err.Error(), `BaseAPI must end with a /v2, /v3 or /v4; setBaseAPI("https://host/v3")`) + ensure.DeepEqual(t, err.Error(), `BaseAPI must end with a /v1, /v2, /v3 or /v4; setBaseAPI("https://host/v3")`) } func TestValidBaseAPI(t *testing.T) { From b7b869f18816de994266024a084e81366b0ebf89 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sat, 19 Oct 2024 21:03:32 +0300 Subject: [PATCH 33/47] fixed TestAddDelBounceList --- bounces_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bounces_test.go b/bounces_test.go index 0c43a8e7..8ee7ef69 100644 --- a/bounces_test.go +++ b/bounces_test.go @@ -127,7 +127,7 @@ func TestAddDelBounceList(t *testing.T) { return false } - createdAt, err := mailgun.NewRFC2822Time("Thu, 13 Oct 2011 18:02:00 UTC") + createdAt, err := mailgun.NewRFC2822Time("Thu, 13 Oct 2011 18:02:00 +0000") if err != nil { t.Fatalf("invalid time") } @@ -162,7 +162,7 @@ func TestAddDelBounceList(t *testing.T) { t.Fatalf("Expected at least one bounce for %s", expect.Address) } t.Logf("Bounce Created At: %s", bounce.CreatedAt) - if !expect.CreatedAt.IsZero() && bounce.CreatedAt != expect.CreatedAt { + if !expect.CreatedAt.IsZero() && !time.Time(bounce.CreatedAt).Equal(time.Time(expect.CreatedAt)) { t.Fatalf("Expected bounce createdAt to be %s, got %s", expect.CreatedAt, bounce.CreatedAt) } } From 196c144b3a3b7014090c91316cdc6c90556af4a3 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sat, 19 Oct 2024 21:05:24 +0300 Subject: [PATCH 34/47] renamed ListMetrics test --- examples_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_test.go b/examples_test.go index bbf84571..8427ca8e 100644 --- a/examples_test.go +++ b/examples_test.go @@ -192,7 +192,7 @@ func ExampleMailgunImpl_VerifyWebhookSignature() { } } -func ExampleMailgunImpl_ListMetrics() { +func ExmplMailgunImpl_ListMetrics() { mg, err := mailgun.NewMailgunFromEnv() if err != nil { log.Fatal(err) From 6f869a0775ce613ce69d8b3f3b681f06f8f971ba Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sat, 19 Oct 2024 21:21:19 +0300 Subject: [PATCH 35/47] switched response to RFC2822Time --- analytics_response.go | 4 ++-- analytics_test.go | 2 +- examples_test.go | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/analytics_response.go b/analytics_response.go index 96ea1436..5696a658 100644 --- a/analytics_response.go +++ b/analytics_response.go @@ -1,8 +1,8 @@ package mailgun type MetricsResponse struct { - Start string `json:"start"` - End string `json:"end"` + Start RFC2822Time `json:"start"` + End RFC2822Time `json:"end"` Resolution Resolution `json:"resolution"` Duration string `json:"duration"` Dimensions []string `json:"dimensions"` diff --git a/analytics_test.go b/analytics_test.go index 0a96c7a4..94668f30 100644 --- a/analytics_test.go +++ b/analytics_test.go @@ -3,7 +3,7 @@ package mailgun var mockAcceptedIncomingCount uint64 = 10 var expectedResponse = MetricsResponse{ - Start: "Mon, 15 Apr 2024 00:00:00 +0000", + // Start: "Mon, 15 Apr 2024 00:00:00 +0000", Dimensions: []string{"time"}, Items: []MetricsItem{ { diff --git a/examples_test.go b/examples_test.go index 8427ca8e..1054ee7d 100644 --- a/examples_test.go +++ b/examples_test.go @@ -192,7 +192,7 @@ func ExampleMailgunImpl_VerifyWebhookSignature() { } } -func ExmplMailgunImpl_ListMetrics() { +func ExampleMailgunImpl_ListMetrics() { mg, err := mailgun.NewMailgunFromEnv() if err != nil { log.Fatal(err) @@ -222,7 +222,8 @@ func ExmplMailgunImpl_ListMetrics() { log.Fatal(iter.Err()) } - fmt.Printf("Page %d:\n", i+1) + fmt.Printf("Page %d: Start: %s; End: %s; Pagination: %+v\n", + i+1, resp.Start, resp.End, resp.Pagination) for _, item := range resp.Items { b, _ := json.Marshal(item) fmt.Printf("%s\n", b) From 76488646e83a0e27fe677f12cc43a1a4323dc762 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sat, 19 Oct 2024 21:23:39 +0300 Subject: [PATCH 36/47] removed not used --- mailgun.go | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/mailgun.go b/mailgun.go index 15c10dc2..3584d00f 100644 --- a/mailgun.go +++ b/mailgun.go @@ -415,33 +415,11 @@ func generateCredentialsUrl(m Mailgun, login string) string { // return fmt.Sprintf("%s/domains/%s/credentials%s", apiBase, m.Domain(), tail) } -// generateStoredMessageUrl generates the URL needed to acquire a copy of a stored message. -func generateStoredMessageUrl(m Mailgun, endpoint, id string) string { - return generateDomainApiUrl(m, fmt.Sprintf("%s/%s", endpoint, id)) - // return fmt.Sprintf("%s/domains/%s/%s/%s", apiBase, m.Domain(), endpoint, id) -} - // generatePublicApiUrl works as generateApiUrl, except that generatePublicApiUrl has no need for the domain. func generatePublicApiUrl(m Mailgun, endpoint string) string { return fmt.Sprintf("%s/%s", m.APIBase(), endpoint) } -// generateParameterizedUrl works as generateApiUrl, but supports query parameters. -func generateParameterizedUrl(m Mailgun, endpoint string, payload payload) (string, error) { - paramBuffer, err := payload.getPayloadBuffer() - if err != nil { - return "", err - } - params := string(paramBuffer.Bytes()) - return fmt.Sprintf("%s?%s", generateApiUrl(m, eventsEndpoint), params), nil -} - -// parseMailgunTime translates a timestamp as returned by Mailgun into a Go standard timestamp. -func parseMailgunTime(ts string) (t time.Time, err error) { - t, err = time.Parse("Mon, 2 Jan 2006 15:04:05 MST", ts) - return -} - // formatMailgunTime translates a timestamp into a human-readable form. func formatMailgunTime(t time.Time) string { return t.Format("Mon, 2 Jan 2006 15:04:05 -0700") From 4057f29ae6d2f1eed135f2aedbe3d575cc823768 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Sat, 19 Oct 2024 21:25:53 +0300 Subject: [PATCH 37/47] updated Mailgun interface --- mailgun.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mailgun.go b/mailgun.go index 3584d00f..54c9b5ea 100644 --- a/mailgun.go +++ b/mailgun.go @@ -151,6 +151,9 @@ type Mailgun interface { DeleteBounce(ctx context.Context, address string) error DeleteBounceList(ctx context.Context) error + ListMetrics(opts MetricsOptions) (*MetricsIterator, error) + + // Deprecated: Use ListMetrics instead. GetStats(ctx context.Context, events []string, opts *GetStatOptions) ([]Stats, error) GetTag(ctx context.Context, tag string) (Tag, error) DeleteTag(ctx context.Context, tag string) error From 281cccc6dd6922bea5a20c8b4450bff5668e5d79 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Mon, 21 Oct 2024 19:51:05 +0300 Subject: [PATCH 38/47] Makefile --- Makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d4028ebb..1060781a 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,10 @@ $(NILAWAY): go install go.uber.org/nilaway/cmd/nilaway@latest .PHONY: all -all: +all: test + +.PHONY: test +test: export GO111MODULE=on; go test . -v .PHONY: godoc From b4a15e8b934b373239ab3ebea728d68f6df6da5b Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Mon, 21 Oct 2024 19:51:51 +0300 Subject: [PATCH 39/47] switched to integration test --- examples_test.go | 46 -------------------------------- go.mod | 3 +++ go.sum | 5 ++-- integration_test.go | 64 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 48 deletions(-) create mode 100644 integration_test.go diff --git a/examples_test.go b/examples_test.go index 1054ee7d..25c41320 100644 --- a/examples_test.go +++ b/examples_test.go @@ -191,49 +191,3 @@ func ExampleMailgunImpl_VerifyWebhookSignature() { os.Exit(1) } } - -func ExampleMailgunImpl_ListMetrics() { - mg, err := mailgun.NewMailgunFromEnv() - if err != nil { - log.Fatal(err) - } - - opts := mailgun.MetricsOptions{ - End: mailgun.RFC2822Time(time.Now().UTC()), - Duration: "30d", - Pagination: mailgun.MetricsPagination{ - Limit: 10, - }, - } - - iter, err := mg.ListMetrics(opts) - if err != nil { - log.Fatal(err) - } - - for i := 0; i < 2; i++ { - func() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) - defer cancel() - - resp := mailgun.MetricsResponse{} - more := iter.Next(ctx, &resp) - if iter.Err() != nil { - log.Fatal(iter.Err()) - } - - fmt.Printf("Page %d: Start: %s; End: %s; Pagination: %+v\n", - i+1, resp.Start, resp.End, resp.Pagination) - for _, item := range resp.Items { - b, _ := json.Marshal(item) - fmt.Printf("%s\n", b) - } - - if !more { - log.Print("no more pages") - os.Exit(0) - } - }() - } - // Output: -} diff --git a/go.mod b/go.mod index b1ac5f5d..647faad4 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/go-chi/chi/v5 v5.0.8 github.com/json-iterator/go v1.1.10 github.com/mailgun/errors v0.3.0 + github.com/stretchr/testify v1.9.0 ) require ( @@ -15,6 +16,8 @@ require ( github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sirupsen/logrus v1.9.0 // indirect golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index f69e9b07..578eb4d1 100644 --- a/go.sum +++ b/go.sum @@ -27,11 +27,12 @@ github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc= golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/integration_test.go b/integration_test.go new file mode 100644 index 00000000..f6ac2a74 --- /dev/null +++ b/integration_test.go @@ -0,0 +1,64 @@ +//go:build integration + +package mailgun_test + +import ( + "context" + "encoding/json" + "log" + "os" + "testing" + "time" + + "github.com/mailgun/mailgun-go/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIntegrationMailgunImpl_ListMetrics(t *testing.T) { + mg, err := mailgun.NewMailgunFromEnv() + if err != nil { + require.NoError(t, err) + } + + opts := mailgun.MetricsOptions{ + End: mailgun.RFC2822Time(time.Now().UTC()), + Duration: "30d", + Pagination: mailgun.MetricsPagination{ + Limit: 10, + }, + } + + iter, err := mg.ListMetrics(opts) + if err != nil { + require.NoError(t, err) + } + + for i := 0; i < 2; i++ { + func() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + resp := mailgun.MetricsResponse{} + more := iter.Next(ctx, &resp) + if iter.Err() != nil { + require.NoError(t, err) + } + + t.Logf("Page %d: Start: %s; End: %s; Pagination: %+v\n", + i+1, resp.Start, resp.End, resp.Pagination) + + assert.GreaterOrEqual(t, len(resp.Items), 1) + + for _, item := range resp.Items { + b, _ := json.Marshal(item) + t.Logf("%s\n", b) + } + + if !more { + log.Print("no more pages") + os.Exit(0) + } + }() + } +} From c1b05f15af69c195cbc280773d49249112d86abf Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Mon, 21 Oct 2024 19:52:30 +0300 Subject: [PATCH 40/47] get rid of "log" package --- integration_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/integration_test.go b/integration_test.go index f6ac2a74..ae9e1c61 100644 --- a/integration_test.go +++ b/integration_test.go @@ -5,7 +5,6 @@ package mailgun_test import ( "context" "encoding/json" - "log" "os" "testing" "time" @@ -56,7 +55,7 @@ func TestIntegrationMailgunImpl_ListMetrics(t *testing.T) { } if !more { - log.Print("no more pages") + t.Log("no more pages") os.Exit(0) } }() From b9a428dd9e2c245f9f15d643f9d14ce38973ddc4 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Thu, 24 Oct 2024 20:46:52 +0300 Subject: [PATCH 41/47] pre-alloc --- analytics.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/analytics.go b/analytics.go index bc233c9d..8f8b1149 100644 --- a/analytics.go +++ b/analytics.go @@ -95,7 +95,9 @@ func (iter *MetricsIterator) fetch(ctx context.Context) error { return err } - var resp MetricsResponse + resp := MetricsResponse{ + Items: make([]MetricsItem, 0, iter.opts.Pagination.Limit), + } err = httpResp.parseFromJSON(&resp) if err != nil { return errors.Wrap(err, "decoding response") From c686e0c3c7de1b84af939ec902ab7c526f0fcd52 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Thu, 24 Oct 2024 20:47:38 +0300 Subject: [PATCH 42/47] added mock --- mailgun.go | 4 ++++ mock.go | 6 ++++++ mock_analytics.go | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 mock_analytics.go diff --git a/mailgun.go b/mailgun.go index 54c9b5ea..876ea4b4 100644 --- a/mailgun.go +++ b/mailgun.go @@ -427,3 +427,7 @@ func generatePublicApiUrl(m Mailgun, endpoint string) string { func formatMailgunTime(t time.Time) string { return t.Format("Mon, 2 Jan 2006 15:04:05 -0700") } + +func ptr[T any](v T) *T { + return &v +} diff --git a/mock.go b/mock.go index 23f08962..11203180 100644 --- a/mock.go +++ b/mock.go @@ -17,6 +17,7 @@ import ( type MockServer interface { Stop() + URL1() string URL4() string URL() string DomainIPS() []string @@ -140,6 +141,7 @@ func NewMockServer() MockServer { ms.addSubaccountRoutes(r) }) ms.addValidationRoutes(r) + ms.addAnalyticsRoutes(r) // Start the server ms.srv = httptest.NewServer(r) @@ -151,6 +153,10 @@ func (ms *mockServer) Stop() { ms.srv.Close() } +func (ms *mockServer) URL1() string { + return ms.srv.URL + "/v1" +} + func (ms *mockServer) URL4() string { return ms.srv.URL + "/v4" } diff --git a/mock_analytics.go b/mock_analytics.go new file mode 100644 index 00000000..1b4ae162 --- /dev/null +++ b/mock_analytics.go @@ -0,0 +1,47 @@ +package mailgun + +import ( + "net/http" + + "github.com/go-chi/chi/v5" +) + +func (ms *mockServer) addAnalyticsRoutes(r chi.Router) { + r.Get("/v1/"+metricsEndpoint, ms.listMetrics) +} + +func (ms *mockServer) listMetrics(w http.ResponseWriter, _ *http.Request) { + start, _ := NewRFC2822Time("Tue, 24 Sep 2024 00:00:00 +0000") + end, _ := NewRFC2822Time("Tue, 24 Oct 2024 00:00:00 +0000") + + resp := MetricsResponse{ + Start: start, + End: end, + Resolution: "day", + Duration: "30d", + Dimensions: []string{"time"}, + Items: []MetricsItem{ + { + Dimensions: []MetricsDimension{{ + Dimension: "time", + Value: "Tue, 24 Sep 2024 00:00:00 +0000", + DisplayValue: "Tue, 24 Sep 2024 00:00:00 +0000", + }}, + Metrics: Metrics{ + SentCount: ptr(uint64(4)), + DeliveredCount: ptr(uint64(3)), + OpenedCount: ptr(uint64(2)), + FailedCount: ptr(uint64(1)), + }, + }, + }, + Pagination: MetricsPagination{ + Sort: "", + Skip: 0, + Limit: 10, + Total: 1, + }, + } + + toJSON(w, resp) +} From 24f007687c443884d26a91f52f5fad9122227795 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Fri, 25 Oct 2024 19:26:20 +0300 Subject: [PATCH 43/47] added unit test --- analytics.go | 13 +++++---- analytics_test.go | 70 +++++++++++++++++++++++++++++++++------------ integration_test.go | 54 ++++++++++++++++------------------ mock_analytics.go | 2 +- 4 files changed, 84 insertions(+), 55 deletions(-) diff --git a/analytics.go b/analytics.go index 8f8b1149..b268b554 100644 --- a/analytics.go +++ b/analytics.go @@ -47,11 +47,11 @@ type MetricsPagination struct { // Colon-separated value indicating column name and sort direction e.g. 'domain:asc'. Sort string `json:"sort"` // The number of items to skip over when satisfying the request. To get the first page of data set skip to zero. Then increment the skip by the limit for subsequent calls. - Skip int64 `json:"skip"` + Skip int `json:"skip"` // The maximum number of items returned in the response. - Limit int64 `json:"limit"` + Limit int `json:"limit"` // The total number of items in the query result set. - Total int64 `json:"total"` + Total int `json:"total"` } type MetricsIterator struct { @@ -68,7 +68,7 @@ func (iter *MetricsIterator) Err() error { // Next retrieves the next page of items from the api. Returns false when there are // no more pages to retrieve or if there was an error. // Use `.Err()` to retrieve the error -func (iter *MetricsIterator) Next(ctx context.Context, resp *MetricsResponse) bool { +func (iter *MetricsIterator) Next(ctx context.Context, resp *MetricsResponse) (more bool) { if iter.err != nil { return false } @@ -79,10 +79,11 @@ func (iter *MetricsIterator) Next(ctx context.Context, resp *MetricsResponse) bo } *resp = iter.resp - if len(iter.resp.Items) == 0 { + iter.opts.Pagination.Skip = iter.opts.Pagination.Skip + iter.opts.Pagination.Limit + + if len(iter.resp.Items) < iter.opts.Pagination.Limit { return false } - iter.opts.Pagination.Skip = iter.opts.Pagination.Skip + iter.opts.Pagination.Limit return true } diff --git a/analytics_test.go b/analytics_test.go index 94668f30..9cf8e348 100644 --- a/analytics_test.go +++ b/analytics_test.go @@ -1,23 +1,55 @@ -package mailgun +package mailgun_test -var mockAcceptedIncomingCount uint64 = 10 +import ( + "context" + "testing" + "time" -var expectedResponse = MetricsResponse{ - // Start: "Mon, 15 Apr 2024 00:00:00 +0000", - Dimensions: []string{"time"}, - Items: []MetricsItem{ - { - Dimensions: []MetricsDimension{ - { - Dimension: "time", - Value: "Mon, 15 Apr 2024 00:00:00 +0000", - DisplayValue: "Mon, 15 Apr 2024 00:00:00 +0000", - }, - }, - Metrics: Metrics{ - AcceptedIncomingCount: &mockAcceptedIncomingCount, - ClickedRate: "0.8300", - }, + "github.com/mailgun/mailgun-go/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// var mockAcceptedIncomingCount uint64 = 10 +// +// var expectedResponse = mailgun.MetricsResponse{ +// // Start: "Mon, 15 Apr 2024 00:00:00 +0000", +// Dimensions: []string{"time"}, +// Items: []MetricsItem{ +// { +// Dimensions: []MetricsDimension{ +// { +// Dimension: "time", +// Value: "Mon, 15 Apr 2024 00:00:00 +0000", +// DisplayValue: "Mon, 15 Apr 2024 00:00:00 +0000", +// }, +// }, +// Metrics: Metrics{ +// AcceptedIncomingCount: &mockAcceptedIncomingCount, +// ClickedRate: "0.8300", +// }, +// }, +// }, +// } + +func TestListMetrics(t *testing.T) { + mg := mailgun.NewMailgun(testDomain, testKey) + mg.SetAPIBase(server.URL1()) + + opts := mailgun.MetricsOptions{ + End: mailgun.RFC2822Time(time.Now().UTC()), + Duration: "30d", + Pagination: mailgun.MetricsPagination{ + Limit: 10, }, - }, + } + it, err := mg.ListMetrics(opts) + require.NoError(t, err) + + var page mailgun.MetricsResponse + ctx := context.Background() + more := it.Next(ctx, &page) + require.Nil(t, it.Err()) + assert.False(t, more) + assert.Len(t, page.Items, 1) } diff --git a/integration_test.go b/integration_test.go index ae9e1c61..e58be40c 100644 --- a/integration_test.go +++ b/integration_test.go @@ -5,7 +5,6 @@ package mailgun_test import ( "context" "encoding/json" - "os" "testing" "time" @@ -29,35 +28,32 @@ func TestIntegrationMailgunImpl_ListMetrics(t *testing.T) { } iter, err := mg.ListMetrics(opts) - if err != nil { - require.NoError(t, err) - } + require.NoError(t, err) + + // create context to list all pages + ctx, cancel := context.WithTimeout(context.Background(), time.Second*60) + defer cancel() for i := 0; i < 2; i++ { - func() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) - defer cancel() - - resp := mailgun.MetricsResponse{} - more := iter.Next(ctx, &resp) - if iter.Err() != nil { - require.NoError(t, err) - } - - t.Logf("Page %d: Start: %s; End: %s; Pagination: %+v\n", - i+1, resp.Start, resp.End, resp.Pagination) - - assert.GreaterOrEqual(t, len(resp.Items), 1) - - for _, item := range resp.Items { - b, _ := json.Marshal(item) - t.Logf("%s\n", b) - } - - if !more { - t.Log("no more pages") - os.Exit(0) - } - }() + var resp mailgun.MetricsResponse + more := iter.Next(ctx, &resp) + if iter.Err() != nil { + require.NoError(t, err) + } + + t.Logf("Page %d: Start: %s; End: %s; Pagination: %+v\n", + i+1, resp.Start, resp.End, resp.Pagination) + + assert.GreaterOrEqual(t, len(resp.Items), 1) + + for _, item := range resp.Items { + b, _ := json.Marshal(item) + t.Logf("%s\n", b) + } + + if !more { + t.Log("no more pages") + break + } } } diff --git a/mock_analytics.go b/mock_analytics.go index 1b4ae162..868bc5b6 100644 --- a/mock_analytics.go +++ b/mock_analytics.go @@ -7,7 +7,7 @@ import ( ) func (ms *mockServer) addAnalyticsRoutes(r chi.Router) { - r.Get("/v1/"+metricsEndpoint, ms.listMetrics) + r.Post("/v1/"+metricsEndpoint, ms.listMetrics) } func (ms *mockServer) listMetrics(w http.ResponseWriter, _ *http.Request) { From e31765f9069731c298826aa5c40934a0dc278d7f Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Fri, 25 Oct 2024 19:43:35 +0300 Subject: [PATCH 44/47] simplified --- analytics.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/analytics.go b/analytics.go index b268b554..9b1f7f5d 100644 --- a/analytics.go +++ b/analytics.go @@ -57,7 +57,6 @@ type MetricsPagination struct { type MetricsIterator struct { opts MetricsOptions req *httpRequest - resp MetricsResponse err error } @@ -73,22 +72,21 @@ func (iter *MetricsIterator) Next(ctx context.Context, resp *MetricsResponse) (m return false } - iter.err = iter.fetch(ctx) + iter.err = iter.fetch(ctx, resp) if iter.err != nil { return false } - *resp = iter.resp iter.opts.Pagination.Skip = iter.opts.Pagination.Skip + iter.opts.Pagination.Limit - if len(iter.resp.Items) < iter.opts.Pagination.Limit { + if len(resp.Items) < iter.opts.Pagination.Limit { return false } return true } -func (iter *MetricsIterator) fetch(ctx context.Context) error { +func (iter *MetricsIterator) fetch(ctx context.Context, resp *MetricsResponse) error { payload := newJSONEncodedPayload(iter.opts) httpResp, err := makePostRequest(ctx, iter.req, payload) @@ -96,15 +94,13 @@ func (iter *MetricsIterator) fetch(ctx context.Context) error { return err } - resp := MetricsResponse{ - Items: make([]MetricsItem, 0, iter.opts.Pagination.Limit), - } - err = httpResp.parseFromJSON(&resp) + // preallocate + resp.Items = make([]MetricsItem, 0, iter.opts.Pagination.Limit) + + err = httpResp.parseFromJSON(resp) if err != nil { return errors.Wrap(err, "decoding response") } - iter.resp = resp - return nil } From 2edd82953231c000d51a58d2e7247c48d9313da1 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Fri, 25 Oct 2024 19:45:14 +0300 Subject: [PATCH 45/47] nil check --- analytics.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/analytics.go b/analytics.go index 9b1f7f5d..1c503114 100644 --- a/analytics.go +++ b/analytics.go @@ -87,6 +87,10 @@ func (iter *MetricsIterator) Next(ctx context.Context, resp *MetricsResponse) (m } func (iter *MetricsIterator) fetch(ctx context.Context, resp *MetricsResponse) error { + if resp == nil { + return errors.New("resp cannot be nil") + } + payload := newJSONEncodedPayload(iter.opts) httpResp, err := makePostRequest(ctx, iter.req, payload) From d88c9b032e71b439c4b173884c1308b470c62254 Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Fri, 25 Oct 2024 20:30:15 +0300 Subject: [PATCH 46/47] assert response --- analytics_test.go | 62 +++++++++++++++++++++++++++-------------------- mailgun_test.go | 4 +++ 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/analytics_test.go b/analytics_test.go index 9cf8e348..1de81e0c 100644 --- a/analytics_test.go +++ b/analytics_test.go @@ -3,46 +3,56 @@ package mailgun_test import ( "context" "testing" - "time" "github.com/mailgun/mailgun-go/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -// var mockAcceptedIncomingCount uint64 = 10 -// -// var expectedResponse = mailgun.MetricsResponse{ -// // Start: "Mon, 15 Apr 2024 00:00:00 +0000", -// Dimensions: []string{"time"}, -// Items: []MetricsItem{ -// { -// Dimensions: []MetricsDimension{ -// { -// Dimension: "time", -// Value: "Mon, 15 Apr 2024 00:00:00 +0000", -// DisplayValue: "Mon, 15 Apr 2024 00:00:00 +0000", -// }, -// }, -// Metrics: Metrics{ -// AcceptedIncomingCount: &mockAcceptedIncomingCount, -// ClickedRate: "0.8300", -// }, -// }, -// }, -// } - func TestListMetrics(t *testing.T) { mg := mailgun.NewMailgun(testDomain, testKey) mg.SetAPIBase(server.URL1()) + start, _ := mailgun.NewRFC2822Time("Tue, 24 Sep 2024 00:00:00 +0000") + end, _ := mailgun.NewRFC2822Time("Tue, 24 Oct 2024 00:00:00 +0000") + opts := mailgun.MetricsOptions{ - End: mailgun.RFC2822Time(time.Now().UTC()), - Duration: "30d", + Start: start, + End: end, Pagination: mailgun.MetricsPagination{ Limit: 10, }, } + + wantResp := mailgun.MetricsResponse{ + Start: start, + End: end, + Resolution: "day", + Duration: "30d", + Dimensions: []string{"time"}, + Items: []mailgun.MetricsItem{ + { + Dimensions: []mailgun.MetricsDimension{{ + Dimension: "time", + Value: "Tue, 24 Sep 2024 00:00:00 +0000", + DisplayValue: "Tue, 24 Sep 2024 00:00:00 +0000", + }}, + Metrics: mailgun.Metrics{ + SentCount: ptr(uint64(4)), + DeliveredCount: ptr(uint64(3)), + OpenedCount: ptr(uint64(2)), + FailedCount: ptr(uint64(1)), + }, + }, + }, + Pagination: mailgun.MetricsPagination{ + Sort: "", + Skip: 0, + Limit: 10, + Total: 1, + }, + } + it, err := mg.ListMetrics(opts) require.NoError(t, err) @@ -51,5 +61,5 @@ func TestListMetrics(t *testing.T) { more := it.Next(ctx, &page) require.Nil(t, it.Err()) assert.False(t, more) - assert.Len(t, page.Items, 1) + assert.Equal(t, wantResp, page) } diff --git a/mailgun_test.go b/mailgun_test.go index 815b4067..9c343eca 100644 --- a/mailgun_test.go +++ b/mailgun_test.go @@ -60,3 +60,7 @@ func TestValidBaseAPI(t *testing.T) { ensure.Nil(t, err) } } + +func ptr[T any](v T) *T { + return &v +} From 519cb672bd1db17608d72d39e0ce2a18ef8e4a1e Mon Sep 17 00:00:00 2001 From: Vilen Topchii <32271530+vtopc@users.noreply.github.com> Date: Fri, 25 Oct 2024 20:38:51 +0300 Subject: [PATCH 47/47] sort --- analytics.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/analytics.go b/analytics.go index 1c503114..3b93f4a6 100644 --- a/analytics.go +++ b/analytics.go @@ -7,6 +7,17 @@ import ( "github.com/mailgun/errors" ) +type MetricsPagination struct { + // Colon-separated value indicating column name and sort direction e.g. 'domain:asc'. + Sort string `json:"sort"` + // The number of items to skip over when satisfying the request. To get the first page of data set skip to zero. Then increment the skip by the limit for subsequent calls. + Skip int `json:"skip"` + // The maximum number of items returned in the response. + Limit int `json:"limit"` + // The total number of items in the query result set. + Total int `json:"total"` +} + // ListMetrics returns domain/account metrics. // // NOTE: Only for v1 API. To use the /v1 version define MG_URL in the environment variable @@ -43,17 +54,6 @@ func (mg *MailgunImpl) ListMetrics(opts MetricsOptions) (*MetricsIterator, error }, nil } -type MetricsPagination struct { - // Colon-separated value indicating column name and sort direction e.g. 'domain:asc'. - Sort string `json:"sort"` - // The number of items to skip over when satisfying the request. To get the first page of data set skip to zero. Then increment the skip by the limit for subsequent calls. - Skip int `json:"skip"` - // The maximum number of items returned in the response. - Limit int `json:"limit"` - // The total number of items in the query result set. - Total int `json:"total"` -} - type MetricsIterator struct { opts MetricsOptions req *httpRequest