added main logic smtp client and response bodies

This commit is contained in:
Pasha 2024-12-03 15:42:15 +03:00
parent 7592367b1e
commit 199549fc7f
10 changed files with 242 additions and 22 deletions

2
.env Normal file

@ -0,0 +1,2 @@
SMTP_API_URL=https://api.smtp.bz/v1
SMTP_API_KEY=P0YsjUB137upXrr1NiJefHmXVKW1hmBWlpev

2
go.mod

@ -6,7 +6,9 @@ require github.com/gofiber/fiber/v2 v2.52.5
require ( require (
github.com/andybalholm/brotli v1.0.5 // indirect 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/google/uuid v1.5.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/klauspost/compress v1.17.0 // indirect github.com/klauspost/compress v1.17.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect

4
go.sum

@ -1,9 +1,13 @@
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 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 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= 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 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=

@ -1,16 +1,12 @@
package client package client
import ( import (
"encoding/json"
"fmt" "fmt"
"gitea.pena/PenaDevops/smtpbiz-exporter/internal/models"
"github.com/gofiber/fiber/v2" "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 { type SMTPClient struct {
apiURL string apiURL string
client *fiber.Client 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) url := fmt.Sprintf("%s/%s", c.apiURL, endpoint)
var req *fiber.Agent if len(params) > 0 {
query := urlPkg.Values{}
switch strings.ToUpper(method) { for key, value := range params {
case fiber.MethodGet: query.Add(key, fmt.Sprintf("%v", value))
// 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)
} }
req = c.client.Post(url) url = fmt.Sprintf("%s?%s", url, query.Encode())
req.Set("Content-Type", "application/json").Body(request)
default:
return nil, fmt.Errorf("unsupported HTTP method: %s", method)
} }
req := c.client.Get(url)
req.Set("Authorization", c.apiKey) req.Set("Authorization", c.apiKey)
statusCode, respBody, errs := req.Bytes() statusCode, respBody, errs := req.Bytes()

@ -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
}

@ -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" // получить список отписчиков
)

88
internal/models/smtp.go Normal file

@ -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"` // Дата и время отправки сообщения
}

0
openapi.yaml Normal file

@ -0,0 +1,2 @@
Суть проблемы, которая будет решаться этим сервисом - периодически так бывает, что заканчивается пакет писем в рассыльщике. Когда это происходит, нам важно быстро узнать об этом и оплатить новый пакет.
Дополнительная информация от этого экспортера - было бы неплохо узнавать, сколько писем оказались в спаме, не были отправлены, оказались невалидны или сколько почтовых ящиков оказалось несуществующими

@ -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))
}