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 (
|
||||
git.tavo.one/tavo/axiom v0.0.0-20250701062914-3086b8fcd640
|
||||
github.com/mattn/go-sqlite3 v1.14.28
|
||||
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/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
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)
|
||||
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