diff --git a/.env b/.env new file mode 100644 index 0000000..659dd1c --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +SMTP_API_URL=https://api.smtp.bz/v1 +SMTP_API_KEY=P0YsjUB137upXrr1NiJefHmXVKW1hmBWlpev \ No newline at end of file diff --git a/go.mod b/go.mod index f7bca9b..e223321 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,9 @@ require github.com/gofiber/fiber/v2 v2.52.5 require ( github.com/andybalholm/brotli v1.0.5 // indirect + github.com/caarlos0/env/v8 v8.0.0 // indirect github.com/google/uuid v1.5.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/klauspost/compress v1.17.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/go.sum b/go.sum index fab6978..8b21421 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,13 @@ github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/caarlos0/env/v8 v8.0.0 h1:POhxHhSpuxrLMIdvTGARuZqR4Jjm8AYmoi/JKlcScs0= +github.com/caarlos0/env/v8 v8.0.0/go.mod h1:7K4wMY9bH0esiXSSHlfHLX5xKGQMnkH5Fk4TDSSSzfo= github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= diff --git a/internal/client/smtp.go b/internal/client/smtp.go index 13799c2..4da02db 100644 --- a/internal/client/smtp.go +++ b/internal/client/smtp.go @@ -1,16 +1,12 @@ package client import ( - "encoding/json" "fmt" + "gitea.pena/PenaDevops/smtpbiz-exporter/internal/models" "github.com/gofiber/fiber/v2" - "strings" + urlPkg "net/url" ) -type Client interface { - UseMethod(method string, endpoint string, params map[string]interface{}) ([]byte, error) -} - type SMTPClient struct { apiURL string client *fiber.Client @@ -25,25 +21,16 @@ func NewSMTPClient(apiURL, apiKey string) *SMTPClient { } } -func (c *SMTPClient) UseMethod(method string, endpoint string, params map[string]interface{}) ([]byte, error) { +func (c *SMTPClient) UseGetMethod(endpoint models.EndpointsSMTP, params map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("%s/%s", c.apiURL, endpoint) - var req *fiber.Agent - - switch strings.ToUpper(method) { - case fiber.MethodGet: - // todo get params - req = c.client.Get(url) - case fiber.MethodPost: - request, err := json.Marshal(params) - if err != nil { - return nil, fmt.Errorf("failed marshal params: %w", err) + if len(params) > 0 { + query := urlPkg.Values{} + for key, value := range params { + query.Add(key, fmt.Sprintf("%v", value)) } - req = c.client.Post(url) - req.Set("Content-Type", "application/json").Body(request) - default: - return nil, fmt.Errorf("unsupported HTTP method: %s", method) + url = fmt.Sprintf("%s?%s", url, query.Encode()) } - + req := c.client.Get(url) req.Set("Authorization", c.apiKey) statusCode, respBody, errs := req.Bytes() diff --git a/internal/initialize/config.go b/internal/initialize/config.go new file mode 100644 index 0000000..56b65fa --- /dev/null +++ b/internal/initialize/config.go @@ -0,0 +1,23 @@ +package initialize + +import ( + "github.com/caarlos0/env/v8" + "github.com/joho/godotenv" + "log" +) + +type Config struct { + SmtpApiUrl string `env:"SMTP_API_URL"` + SmtpApiKey string `env:"SMTP_API_KEY"` +} + +func LoadConfig() (*Config, error) { + if err := godotenv.Load(); err != nil { + log.Print("No .env file found") + } + var config Config + if err := env.Parse(&config); err != nil { + return nil, err + } + return &config, nil +} diff --git a/internal/models/endpoints.go b/internal/models/endpoints.go new file mode 100644 index 0000000..dab4260 --- /dev/null +++ b/internal/models/endpoints.go @@ -0,0 +1,14 @@ +package models + +type EndpointsSMTP string + +const ( + UserDataEndpoint EndpointsSMTP = "user" // данные по подьзователю + UserStatsEndpoint EndpointsSMTP = "user/stats" // статистика по рассылкам + UserDomainsEndpoint EndpointsSMTP = "user/domain" // домены отправителя + UserIPsEndpoint EndpointsSMTP = "user/ip" // выделенные ip-адреса отправителя + + LogMsgEndpoint EndpointsSMTP = "log/message" // получение отправленных писем с гет параметрами + + UnsubscribeEndpoint EndpointsSMTP = "unsubscribe" // получить список отписчиков +) diff --git a/internal/models/smtp.go b/internal/models/smtp.go new file mode 100644 index 0000000..07348f2 --- /dev/null +++ b/internal/models/smtp.go @@ -0,0 +1,88 @@ +package models + +type UserDataResponse struct { + HSent int64 `json:"hsent"` // Количество писем, отправленных за последний час + HLimit int64 `json:"hlimit"` // Лимит на количество писем, которые можно отправить за час + DSent int64 `json:"dsent"` // Количество писем, отправленных за текущий день + DLimit int64 `json:"dlimit"` // Лимит на количество писем, которые можно отправить за день + Quota int64 `json:"quota"` // Оставшееся количество писем, которые можно отправить в рамках текущего тарифа + Validate int64 `json:"validate"` // Лимит на количество проверок наверное емейл адресов + Tariff string `json:"tarif"` // Название текущего тарифа + ExpiresQuota string `json:"expires_quota"` // Дата окончания квоты + TariffQuota int64 `json:"tarif_quota"` // Общая квота на отправку писем, предоставляемая по тарифу + Balance float64 `json:"balance"` // Баланс bucks + TariffPrice float64 `json:"tarif_price"` // Тарифный план, в месяц +} + +type UserStatsResponse struct { + Sent int64 `json:"sent"` // Общее количество отправленных писем + Open int64 `json:"open"` // Количество открытых писем + Spam int64 `json:"spam"` // Количество писем, попавших в спам + Bounce int64 `json:"bounce"` // Количество писем, которые вернулись как недоставленные + Unsub int64 `json:"unsub"` // Количество отписавшихся пользователей + Tracking TrackingStats `json:"tracking"` +} + +type TrackingStats struct { + Device DeviceStats `json:"device"` // Статистика по устройствам + Activity ActivityStats `json:"activity"` // Активность пользователей в разное время суток + Country map[string]int64 `json:"country"` // Статистика по странам (код страны и количество взаимодействий) +} + +type DeviceStats struct { + Computer int64 `json:"computer"` // Количество взаимодействий с компьютеров + Tablet int64 `json:"tablet"` // Количество взаимодействий с планшетов + Mobile int64 `json:"mobile"` // Количество взаимодействий с мобильных устройств +} + +type ActivityStats struct { + Morning int64 `json:"morning"` // Взаимодействия утром + Day int64 `json:"day"` // Взаимодействия днем + Evening int64 `json:"evening"` // Взаимодействия вечером + Night int64 `json:"night"` // Взаимодействия ночью +} + +type DomainsResponse []DomainInfo + +type DomainInfo struct { + Domain string `json:"domain"` // Имя домена + SPF bool `json:"spf"` // SPF запись + DKIM bool `json:"dkim"` // DKIM запись + CNAME bool `json:"cname"` // CNAME запись + IsModeration bool `json:"isModeration"` // Статус модерации + IsActive bool `json:"isActive"` // Статус активности домена +} + +// один из возможных ответов - {"result":false} +// стоит чекать на длину массив +type IpsResponse []IPInfo + +type IPInfo struct { + Sent int64 `json:"sent"` // Количество отправленных писем + Bounce int64 `json:"bounce"` // Количество возвратов + IP string `json:"ip"` // IP адрес + IsInstall bool `json:"isInstall"` // Установлен ли IP + IsActive bool `json:"isActive"` // Активен ли IP + ExpiresDate string `json:"expiresDate"` // Дата окончания + CreateDate string `json:"createDate"` // Дата создания +} + +type LogMsgResponse struct { + Data []LogMsgData `json:"data"` + TotalPages int64 `json:"totalPages"` // Всего страниц + TotalCount int64 `json:"totalCount"` // Всего записей + PerPagesCount int64 `json:"perPagesCount"` // Количество записей на странице +} + +type LogMsgData struct { + MailFrom string `json:"mailfrom"` // От кого отправлено письмо + MailTo string `json:"mailto"` // Кому отправлено письмо + Status string `json:"status"` // Статус письма + IsOpen bool `json:"is_open"` // Было ли письмо открыто + Subject string `json:"subject"` // Тема письма + IsUnsubscribe bool `json:"is_unsubscribe"` // Является ли письмо отпиской + Tag string `json:"tag"` // Тег сообщения + Response string `json:"response"` // Ответ от сервера + MessageID string `json:"messageid"` // Уникальный идентификатор сообщения + Date string `json:"date"` // Дата и время отправки сообщения +} diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 0000000..e69de29 diff --git a/readme.md b/readme.md index e69de29..1c8708a 100644 --- a/readme.md +++ b/readme.md @@ -0,0 +1,2 @@ +Суть проблемы, которая будет решаться этим сервисом - периодически так бывает, что заканчивается пакет писем в рассыльщике. Когда это происходит, нам важно быстро узнать об этом и оплатить новый пакет. +Дополнительная информация от этого экспортера - было бы неплохо узнавать, сколько писем оказались в спаме, не были отправлены, оказались невалидны или сколько почтовых ящиков оказалось несуществующими \ No newline at end of file diff --git a/tests/integration/client_test.go b/tests/integration/client_test.go new file mode 100644 index 0000000..4a30828 --- /dev/null +++ b/tests/integration/client_test.go @@ -0,0 +1,98 @@ +package integration + +import ( + "encoding/json" + "fmt" + "gitea.pena/PenaDevops/smtpbiz-exporter/internal/client" + "gitea.pena/PenaDevops/smtpbiz-exporter/internal/models" + "testing" +) + +const apiUrl = "https://api.smtp.bz/v1" +const apiKey = "8tv2xcsfCMBX3TCQxzgeeEwAEYyQrPUp0ggw" + +func TestUserData(t *testing.T) { + smtp := client.NewSMTPClient(apiUrl, apiKey) + + resp, err := smtp.UseGetMethod(models.UserDataEndpoint, map[string]interface{}{}) + if err != nil { + t.Fatal(err) + } + fmt.Println(string(resp)) +} + +func TestUserStats(t *testing.T) { + smtp := client.NewSMTPClient(apiUrl, apiKey) + + resp, err := smtp.UseGetMethod(models.UserStatsEndpoint, map[string]interface{}{}) + if err != nil { + t.Fatal(err) + } + fmt.Println(string(resp)) +} + +func TestUserDomains(t *testing.T) { + smtp := client.NewSMTPClient(apiUrl, apiKey) + + resp, err := smtp.UseGetMethod(models.UserDomainsEndpoint, map[string]interface{}{}) + if err != nil { + t.Fatal(err) + } + fmt.Println(string(resp)) +} + +func TestUserIPs(t *testing.T) { + smtp := client.NewSMTPClient(apiUrl, apiKey) + + resp, err := smtp.UseGetMethod(models.UserIPsEndpoint, map[string]interface{}{}) + if err != nil { + t.Fatal(err) + } + fmt.Println(string(resp)) +} + +// todo рейтлимитер нужен я думаю +func TestLogMsg(t *testing.T) { + smtp := client.NewSMTPClient(apiUrl, apiKey) + + limit := 50 + offset := 0 + var maxCount int64 + + for { + resp, err := smtp.UseGetMethod(models.LogMsgEndpoint, map[string]interface{}{ + "limit": limit, + "offset": offset, + }) + if err != nil { + t.Fatal(err) + } + + var result models.LogMsgResponse + err = json.Unmarshal(resp, &result) + if err != nil { + t.Fatal(err) + } + fmt.Println(result) + + if maxCount == 0 { + maxCount = result.TotalCount + } + + if int64(offset+limit) >= maxCount { + break + } + offset += limit + fmt.Println(offset) + } +} + +func TestUnsubscribe(t *testing.T) { + smtp := client.NewSMTPClient(apiUrl, apiKey) + + resp, err := smtp.UseGetMethod(models.UnsubscribeEndpoint, map[string]interface{}{}) + if err != nil { + t.Fatal(err) + } + fmt.Println(string(resp)) +}