old mailer code

This commit is contained in:
Risotto Bias 2025-01-12 06:03:17 -07:00
parent a96de40dd3
commit ed741e12c3
8 changed files with 363 additions and 6 deletions

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

55
assemble.go Normal file
View file

@ -0,0 +1,55 @@
package mailer
import "fmt"
/*
use AddTemplate and AddKeyedTemplate to make repeatable emails
*/
// Template creates a repeatable template of your subject/body
func (m Mailer) AddTemplate(name string, subject string, body string) {
m.Send[name] = func(user string) {
m.sendNoKey(user, subject, body)
}
}
// TemplateKeyed returns a function that sends keyed emails
func (m Mailer) AddKeyedTemplate(name, subject, explanation, baseurl, notyou string) {
m.SendKeyed[name] = func(username string, code string) {
m.sendKeyed(username, subject, explanation, baseurl, code, notyou)
}
}
// pasting body together
func (m Mailer) sendKeyed(username string, subject string, explanation string, baseurl string, code string, notyou string) {
fmt.Println("send keyed: " + subject + " " + code + " to " + username)
message := explanation + "\n" +
baseurl + code + "\n\n" +
notyou + "\n\n"
msg := m.assemble(username, subject, message)
m.internal_mail(username, msg)
}
func (m Mailer) sendNoKey(username string, subject string, body string) {
message := body + "\n\n"
msg := m.assemble(username, subject, message)
m.internal_mail(username, msg)
}
// asemble adds the unsubscribe codes and footer to an email
func (m Mailer) assemble(to string, subj string, message string) []byte {
code := m.UnsubscribeCodeFn(to)
msg := []byte("To: " + to + "\r\n" +
"From: " + m.From + "\r\n" +
"Subject: " + subj + "\r\n" +
"List-Unsubscribe post: List-Unsubscribe=One click; \r\n" +
"List-Unsubscribe: <" + m.UnsubscribeUrl + code + ">\r\n" +
"\r\n" +
message +
m.Footer +
"If you want to unsubscribe, reply to this email or use this link:\n\n" +
m.UnsubscribeUrl + code + "\n\n" +
"\r\n" + "\r\n",
)
return msg
}

133
delay/delay.go Normal file
View file

@ -0,0 +1,133 @@
package delay
import (
"errors"
"fmt"
"sync/atomic"
"time"
)
var GLOBAL_MAILCOUNT *mailqueue
type mailer struct {
subject string
to string
body string
}
// probably easier to do this as sqlite storage selections
type mailqueue struct {
dirty atomic.Bool
size int
instant chan mailer // priority zero
reminder chan mailer // priority one
maintenance chan mailer // priority two
marketing chan mailer // priority 3
errors int
maxerrors int
}
func init() {
var err error
GLOBAL_MAILCOUNT, err = NewMailqueue(100, 60, 90, 5)
if err != nil {
fmt.Println(err)
}
}
func NewMailqueue(size int, resetseconds int, sendseconds int, maxerrors int) (*mailqueue, error) {
m := mailqueue{
size: size,
instant: make(chan mailer, size),
reminder: make(chan mailer, size),
maintenance: make(chan mailer, size),
marketing: make(chan mailer, size),
maxerrors: maxerrors,
}
if resetseconds < 60 {
return nil, errors.New("too small of a value")
}
if sendseconds < 90 {
return nil, errors.New("too large of a value")
}
go m.BucketDrip(resetseconds)
go m.BackgroundMail(sendseconds)
return &m, nil
}
func (m *mailqueue) BucketDrip(resetseconds int) {
// every 60 seconds
for range time.Tick(time.Second * time.Duration(resetseconds)) {
m.dirty.Store(false)
}
}
func (m *mailqueue) BackgroundMail(sendseconds int) {
// every 90 seconds (.75*60*24, 1080)
for range time.Tick(time.Second * time.Duration(sendseconds)) {
if m.errors > m.maxerrors {
fmt.Println("too many errors, not sending.")
return
}
var err error
if m.dirty.Load() != true {
select {
case e := <-m.maintenance:
m.dirty.Store(true)
err = Send(e)
default:
select {
case e := <-m.maintenance:
m.dirty.Store(true)
err = Send(e)
case e := <-m.reminder:
m.dirty.Store(true)
err = Send(e)
default:
select {
case e := <-m.maintenance:
m.dirty.Store(true)
err = Send(e)
case e := <-m.reminder:
m.dirty.Store(true)
err = Send(e)
case e := <-m.marketing:
m.dirty.Store(true)
err = Send(e)
default:
fmt.Println("queue empty")
}
}
}
}
if err != nil {
// add to failed queue? store as failed?
m.errors += 1
}
err = nil
}
}
func (m *mailqueue) AddSend(mail mailer, queue string) error {
var target chan mailer
switch queue {
case "maintenance":
target = m.maintenance
case "reminder":
target = m.reminder
case "marketing":
target = m.marketing
default:
return errors.New("no valid queue")
}
if m.size-1 < len(target) {
return errors.New("mail queue full!")
}
select {
case target <- mail:
default:
return errors.New("mail queue full!")
}
return nil
}
func Send(m mailer) error {
fmt.Println(m.to, m.subject, m.body)
return nil
}

49
example_test.go Normal file
View file

@ -0,0 +1,49 @@
package mailer_test
import (
"crypto/sha256"
"fmt"
"git.bivouac.wiki/use/mailer"
)
// UnsubFunc creates a code for the user to manage their email preferences
func UnsubFunc(user string) string {
// you could also just as well store random UUIDs in a KV...
salt := "example_code"
h := sha256.New()
// the important part is not being able to reverse the username from the code
h.Write([]byte(user + salt))
hash := fmt.Sprintf("%x", h.Sum(nil))
return hash
}
func ExampleMailer() {
from := "app@ornithopterhosting.com"
pass := "password1234"
host := "mail.ornithopterhosting.com"
m := mailer.New(from,
pass,
host,
465,
"https://app.ornithopter.me/unsubscribe/",
"If you have problems contact cameron@ornithopterhosting.com",
UnsubFunc,
)
// build methods
notyou := "If this wasn't you, please email us at support@ornithopterhosting.com"
register := "Welcome! Click this link to confirm your account:"
registerbase := "https://app.ornithopter.me/auth/register/"
m.AddKeyedTemplate("Register", "Registration", register, registerbase, notyou)
recoveryexplain := "Someone (probably you) requested a password reset.\nTo finish recovering your account, click here:"
recoverybase := "https://app.ornithopter.me/auth/recover/"
m.AddKeyedTemplate("Recovery", "Password Recovery", recoveryexplain, recoverybase, notyou)
passwordchanged := "Someone (probably you) changed your password.\n" +
"If this wasn't you, please email us at support@ornithopterhosting.com\n"
m.AddTemplate("Changed", "Password Changed", passwordchanged)
// send with methods
m.SendKeyed["Register"]("user@foobar.com", "1234")
m.SendKeyed["Recovery"]("user@foobar.com", "1234")
m.Send["Changed"]("user@foobar.com")
}

37
init.go Normal file
View file

@ -0,0 +1,37 @@
package mailer
import (
"strconv"
"sync/atomic"
)
type Mailer struct {
Password string
HostOnly string
HostWithPort string
From string
Footer string
UnsubscribeUrl string
UnsubscribeCodeFn UnsubFunc
Send map[string]func(email string)
SendKeyed map[string]func(email string, code string)
mail_counter *atomic.Uint64
wanted_send *atomic.Uint64
}
// UnsubscribeFunc should take a username and create an opaque string
type UnsubFunc func(username string) string
// New creates a mailer with your configured settings
func New(from string, password string, host string, port int, unsubscribe string, footer string, unsub UnsubFunc) Mailer {
p := strconv.FormatInt(int64(port), 10)
return Mailer{
Password: password, // utils.GlobalConfig.SMTP_PASSWORD
From: from,
HostOnly: host,
HostWithPort: host + ":" + p,
Footer: footer,
UnsubscribeUrl: unsubscribe,
UnsubscribeCodeFn: unsub,
}
}

68
internal_mail.go Normal file
View file

@ -0,0 +1,68 @@
package mailer
import (
"crypto/tls"
"fmt"
"net/smtp"
"strconv"
)
// internal SMTP code, messy.
func (m Mailer) internal_mail(un string, msg []byte) {
m.wanted_send.Add(1)
wanted := m.wanted_send.Load()
mail_amt := m.mail_counter.Load()
fmt.Println("sent mail: " + strconv.Itoa(int(mail_amt)))
fmt.Println("wanted to send: " + strconv.Itoa(int(wanted)))
if mail_amt > 3000 {
fmt.Println("sending total too high.")
return
}
recip := []string{un}
auth := smtp.PlainAuth(m.From, m.From, m.Password, m.HostWithPort)
tlsconfig := &tls.Config{
InsecureSkipVerify: true,
ServerName: m.HostOnly,
}
conn, err := tls.Dial("tcp", m.HostWithPort, tlsconfig)
if err != nil {
fmt.Println("dial failed")
return
}
client, err := smtp.NewClient(conn, m.HostWithPort)
if err != nil {
fmt.Println("new client failed")
return
}
if err = client.Auth(auth); err != nil {
fmt.Println("auth failed")
return
}
if err = client.Mail(m.From); err != nil {
fmt.Println("from failed")
return
}
client.Rcpt(recip[0])
w, err := client.Data()
if err != nil {
fmt.Println("recip failed")
return
}
_, err = w.Write(msg)
if err != nil {
fmt.Println("open body failed")
return
}
err = w.Close()
if err != nil {
fmt.Println("close body failed")
return
}
err = client.Quit()
if err != nil {
fmt.Println("client quit failed")
return
}
fmt.Println("sent!")
m.mail_counter.Add(1)
}

3
kv.go
View file

@ -1,3 +0,0 @@
package mailer
// kv interface (for unsubscribe links)

View file

@ -1,3 +0,0 @@
package mailer
// smtp code