(Feat): Initial Commit
Some checks failed
Build & Test / build-test (push) Has been cancelled
Build & Test / swagger-codegen-cli (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled

This commit is contained in:
2026-07-03 19:41:31 +05:30
commit 7e940c83a7
461 changed files with 45002 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
package transport
import (
"errors"
"fmt"
"log"
"sync"
"time"
"allaboutapps.dev/aw/go-starter/internal/util"
"github.com/jordan-wright/email"
)
const defaultWaitTimeout = time.Second * 10
type MockMailTransport struct {
sync.RWMutex
mails []*email.Email
OnMailSent func(mail email.Email) // non pointer to prevent concurrent read errors
wg sync.WaitGroup
expected int
}
func NewMock() *MockMailTransport {
return &MockMailTransport{
RWMutex: sync.RWMutex{},
mails: make([]*email.Email, 0),
OnMailSent: func(_ email.Email) {},
}
}
func (m *MockMailTransport) Send(mail *email.Email) error {
m.Lock()
defer m.Unlock()
// Calling wg.Done might panic leaving a user clueless what was the reason of test failure.
// We will add more information before exiting.
defer func() {
rcp := recover()
if rcp == nil {
return
}
err, ok := rcp.(error)
if !ok {
err = fmt.Errorf("%v", rcp)
}
log.Fatalf("Unexpected email sent! MockMailTransport panicked: %s", err)
}()
m.mails = append(m.mails, mail)
m.OnMailSent(*mail)
if m.expected > 0 {
m.wg.Done()
}
return nil
}
func (m *MockMailTransport) GetLastSentMail() *email.Email {
m.RLock()
defer m.RUnlock()
if len(m.mails) == 0 {
return nil
}
return m.mails[len(m.mails)-1]
}
func (m *MockMailTransport) GetSentMails() []*email.Email {
m.RLock()
defer m.RUnlock()
return m.mails
}
// Expect adds the mailCnt to a waitgroup. Done() is called by Send
func (m *MockMailTransport) Expect(mailCnt int) {
m.expected = mailCnt
m.wg.Add(mailCnt)
}
// Wait until all expected mails have arrived
func (m *MockMailTransport) Wait() {
m.WaitWithTimeout(defaultWaitTimeout)
}
func (m *MockMailTransport) WaitWithTimeout(timeout time.Duration) {
if err := util.WaitTimeout(&m.wg, timeout); errors.Is(err, util.ErrWaitTimeout) {
log.Fatalf("Timeout waiting for %d mails to be sent", m.expected)
}
}

View File

@@ -0,0 +1,56 @@
package transport
import (
"fmt"
"net"
"net/smtp"
"strconv"
"github.com/jordan-wright/email"
)
type SMTPMailTransport struct {
config SMTPMailTransportConfig
addr string
auth smtp.Auth
}
func NewSMTP(config SMTPMailTransportConfig) *SMTPMailTransport {
mailTransport := &SMTPMailTransport{
config: config,
addr: net.JoinHostPort(config.Host, strconv.Itoa(config.Port)),
auth: nil,
}
switch config.AuthType {
case SMTPAuthTypePlain:
mailTransport.auth = smtp.PlainAuth("", config.Username, config.Password, config.Host)
case SMTPAuthTypeCRAMMD5:
mailTransport.auth = smtp.CRAMMD5Auth(config.Username, config.Password)
case SMTPAuthTypeLogin:
mailTransport.auth = LoginAuth(config.Username, config.Password, config.Host)
}
return mailTransport
}
func (m *SMTPMailTransport) Send(mail *email.Email) error {
var err error
switch m.config.Encryption {
case SMTPEncryptionNone:
err = mail.Send(m.addr, m.auth)
case SMTPEncryptionTLS:
err = mail.SendWithTLS(m.addr, m.auth, m.config.TLSConfig)
case SMTPEncryptionStartTLS:
err = mail.SendWithStartTLS(m.addr, m.auth, m.config.TLSConfig)
default:
return fmt.Errorf("invalid SMTP encryption %q", m.config.Encryption)
}
if err != nil {
return fmt.Errorf("failed to send email: %w", err)
}
return nil
}

View File

@@ -0,0 +1,81 @@
package transport
import (
"crypto/tls"
"fmt"
"strings"
)
type SMTPAuthType int
const (
// SMTPAuthTypeNone indicates no SMTP authentication should be performed.
SMTPAuthTypeNone SMTPAuthType = iota
// SMTPAuthTypePlain indicates SMTP authentication should be performed using the "AUTH PLAIN" protocol.
SMTPAuthTypePlain
// SMTPAuthTypeCRAMMD5 indicates SMTP authentication should be performed using the "CRAM-MD5" protocol.
SMTPAuthTypeCRAMMD5
// SMTPAuthTypeLogin indicates SMTP authentication should be performed using the "LOGIN" protocol.
SMTPAuthTypeLogin
)
func (t SMTPAuthType) String() string {
switch t {
case SMTPAuthTypeNone:
return "none"
case SMTPAuthTypePlain:
return "plain"
case SMTPAuthTypeCRAMMD5:
return "cram-md5"
case SMTPAuthTypeLogin:
return "login"
default:
return fmt.Sprintf("unknown (%d)", t)
}
}
func SMTPAuthTypeFromString(s string) SMTPAuthType {
switch strings.ToLower(s) {
case "plain":
return SMTPAuthTypePlain
case "cram-md5":
return SMTPAuthTypeCRAMMD5
case "login":
return SMTPAuthTypeLogin
default:
return SMTPAuthTypeNone
}
}
type SMTPEncryption string
const (
SMTPEncryptionNone SMTPEncryption = "none"
SMTPEncryptionTLS SMTPEncryption = "tls"
SMTPEncryptionStartTLS SMTPEncryption = "starttls"
)
func (e SMTPEncryption) String() string {
return string(e)
}
func SMTPEncryptionFromString(s string) SMTPEncryption {
switch strings.ToLower(s) {
case "tls":
return SMTPEncryptionTLS
case "starttls":
return SMTPEncryptionStartTLS
default:
return SMTPEncryptionNone
}
}
type SMTPMailTransportConfig struct {
Host string
Port int
AuthType SMTPAuthType `json:"-"` // iota
Username string
Password string `json:"-"` // sensitive
Encryption SMTPEncryption `json:"-"` // iota
TLSConfig *tls.Config `json:"-"` // pointer
}

View File

@@ -0,0 +1,50 @@
package transport
import (
"errors"
"fmt"
"net/smtp"
)
type loginAuth struct {
username []byte
password []byte
host string
}
func LoginAuth(username string, password string, host ...string) smtp.Auth {
auth := &loginAuth{
username: []byte(username),
password: []byte(password),
host: "",
}
if len(host) > 0 {
auth.host = host[0]
}
return auth
}
func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
if len(a.host) > 0 && server.Name != a.host {
return "", nil, errors.New("wrong host name")
}
return "LOGIN", a.username, nil
}
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if more {
switch string(fromServer) {
case "Username:":
return a.username, nil
case "Password:":
return a.password, nil
default:
return nil, fmt.Errorf("unknown challenge received from server: %q", fromServer)
}
}
return nil, nil
}

View File

@@ -0,0 +1,7 @@
package transport
import "github.com/jordan-wright/email"
type MailTransporter interface {
Send(mail *email.Email) error
}