separate example, add mail module
This commit is contained in:
parent
3086b8fcd6
commit
238017b244
23 changed files with 222 additions and 2 deletions
|
@ -1,8 +1,9 @@
|
||||||
module git.tavo.one/tavo/axiom
|
module axiom-fullstack
|
||||||
|
|
||||||
go 1.22.1
|
go 1.24.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
git.tavo.one/tavo/axiom v0.0.0-20250701062914-3086b8fcd640
|
||||||
github.com/mattn/go-sqlite3 v1.14.28
|
github.com/mattn/go-sqlite3 v1.14.28
|
||||||
github.com/tdewolff/minify/v2 v2.23.8
|
github.com/tdewolff/minify/v2 v2.23.8
|
||||||
)
|
)
|
|
@ -1,3 +1,5 @@
|
||||||
|
git.tavo.one/tavo/axiom v0.0.0-20250701062914-3086b8fcd640 h1:0M5+uKndwQi5U43n+C0botOx4sfnU+2Mav9yvEQzhDs=
|
||||||
|
git.tavo.one/tavo/axiom v0.0.0-20250701062914-3086b8fcd640/go.mod h1:jeOZoBsGgvFJ3XlPEzs0yYMNZwWcsYUbYjDJ0qKW1FM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
|
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
|
||||||
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/tdewolff/minify/v2 v2.23.8 h1:tvjHzRer46kwOfpdCBCWsDblCw3QtnLJRd61pTVkyZ8=
|
github.com/tdewolff/minify/v2 v2.23.8 h1:tvjHzRer46kwOfpdCBCWsDblCw3QtnLJRd61pTVkyZ8=
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
203
mail/smtp.go
Normal file
203
mail/smtp.go
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
package smtp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/smtp"
|
||||||
|
"net/textproto"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Auth struct {
|
||||||
|
Host string
|
||||||
|
Port string
|
||||||
|
Pass string
|
||||||
|
Debug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func Client(host, port, password string) *Auth {
|
||||||
|
return &Auth{
|
||||||
|
Host: host,
|
||||||
|
Port: port,
|
||||||
|
Pass: password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Auth) tlsConfig() *tls.Config {
|
||||||
|
return &tls.Config{ServerName: s.Host}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Auth) smtpClient() (*smtp.Client, error) {
|
||||||
|
address := s.Host + ":" + s.Port
|
||||||
|
portNum, err := strconv.Atoi(s.Port)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid port number: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if portNum == 465 {
|
||||||
|
conn, err := tls.Dial("tcp", address, s.tlsConfig())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to SMTPS server: %w", err)
|
||||||
|
}
|
||||||
|
return smtp.NewClient(conn, s.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := smtp.Dial(address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok, _ := client.Extension("STARTTLS"); ok {
|
||||||
|
if err := client.StartTLS(s.tlsConfig()); err != nil {
|
||||||
|
client.Close()
|
||||||
|
return nil, fmt.Errorf("failed to start TLS: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
client.Close()
|
||||||
|
return nil, fmt.Errorf("TLS not supported by the server")
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Auth) SendText(
|
||||||
|
from string,
|
||||||
|
to []string,
|
||||||
|
subject string,
|
||||||
|
body string,
|
||||||
|
attachments map[string]*bytes.Buffer,
|
||||||
|
) error {
|
||||||
|
return s.sendMessage(from, to, subject, body, "text/plain; charset=utf-8", attachments)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Auth) SendHTML(
|
||||||
|
from string,
|
||||||
|
to []string,
|
||||||
|
subject string,
|
||||||
|
html string,
|
||||||
|
attachments map[string]*bytes.Buffer,
|
||||||
|
) error {
|
||||||
|
return s.sendMessage(from, to, subject, html, "text/html; charset=utf-8", attachments)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Auth) sendMessage(
|
||||||
|
from string,
|
||||||
|
to []string,
|
||||||
|
subject string,
|
||||||
|
body string,
|
||||||
|
contentType string,
|
||||||
|
attachments map[string]*bytes.Buffer,
|
||||||
|
) error {
|
||||||
|
message, err := s.buildMessage(from, to, subject, body, contentType, attachments)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create email message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := s.smtpClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
auth := smtp.PlainAuth("", from, s.Pass, s.Host)
|
||||||
|
if err = client.Auth(auth); err != nil {
|
||||||
|
return fmt.Errorf("authentication failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.Mail(from); err != nil {
|
||||||
|
return fmt.Errorf("failed to set sender: %w", err)
|
||||||
|
}
|
||||||
|
for _, recipient := range to {
|
||||||
|
if err = client.Rcpt(recipient); err != nil {
|
||||||
|
return fmt.Errorf("failed to add recipient %s: %w", recipient, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w, err := client.Data()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get data writer: %w", err)
|
||||||
|
}
|
||||||
|
if _, err = w.Write(message); err != nil {
|
||||||
|
return fmt.Errorf("failed to write message: %w", err)
|
||||||
|
}
|
||||||
|
if err = w.Close(); err != nil {
|
||||||
|
return fmt.Errorf("failed to send message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.Quit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Auth) Validate(user string) error {
|
||||||
|
client, err := s.smtpClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
auth := smtp.PlainAuth("", user, s.Pass, s.Host)
|
||||||
|
if err := client.Auth(auth); err != nil {
|
||||||
|
return fmt.Errorf("authentication failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Auth) buildMessage(
|
||||||
|
from string,
|
||||||
|
to []string,
|
||||||
|
subject string,
|
||||||
|
body string,
|
||||||
|
contentType string,
|
||||||
|
attachments map[string]*bytes.Buffer,
|
||||||
|
) ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
writer := multipart.NewWriter(&buf)
|
||||||
|
|
||||||
|
buf.WriteString(fmt.Sprintf("From: %s\r\n", from))
|
||||||
|
buf.WriteString(fmt.Sprintf("To: %s\r\n", strings.Join(to, ",")))
|
||||||
|
buf.WriteString(fmt.Sprintf("Subject: %s\r\n", subject))
|
||||||
|
buf.WriteString("MIME-Version: 1.0\r\n")
|
||||||
|
buf.WriteString(fmt.Sprintf("Content-Type: multipart/mixed; boundary=%s\r\n\r\n", writer.Boundary()))
|
||||||
|
|
||||||
|
if err := s.write(writer, contentType, body); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write email body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for filename, fileBuffer := range attachments {
|
||||||
|
if err := s.attach(writer, filename, fileBuffer); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to attach file %s: %w", filename, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writer.Close(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to close writer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Auth) write(writer *multipart.Writer, contentType, content string) error {
|
||||||
|
partHeader := make(textproto.MIMEHeader)
|
||||||
|
partHeader.Set("Content-Type", contentType)
|
||||||
|
part, err := writer.CreatePart(partHeader)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create part: %w", err)
|
||||||
|
}
|
||||||
|
_, err = part.Write([]byte(content))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Auth) attach(writer *multipart.Writer, filename string, fileBuffer *bytes.Buffer) error {
|
||||||
|
attachmentHeader := make(textproto.MIMEHeader)
|
||||||
|
attachmentHeader.Set("Content-Type", "application/octet-stream")
|
||||||
|
attachmentHeader.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
|
||||||
|
attachment, err := writer.CreatePart(attachmentHeader)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create attachment part: %w", err)
|
||||||
|
}
|
||||||
|
_, err = attachment.Write(fileBuffer.Bytes())
|
||||||
|
return err
|
||||||
|
}
|
|
@ -84,3 +84,17 @@ func RenderHTML(w http.ResponseWriter, r *http.Request, name string, data any) e
|
||||||
_, err := io.Copy(w, &buf)
|
_, err := io.Copy(w, &buf)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RenderHTMLToString(name string, data any) (string, error) {
|
||||||
|
tmpl, ok := TemplateCache[name]
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("template %q not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := tmpl.ExecuteTemplate(&buf, tmpl.Name(), data); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to execute template %q: %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue