old mailer code
This commit is contained in:
parent
a96de40dd3
commit
ed741e12c3
8 changed files with 363 additions and 6 deletions
21
LICENSE
Normal file
21
LICENSE
Normal 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
55
assemble.go
Normal 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
133
delay/delay.go
Normal 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
49
example_test.go
Normal 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
37
init.go
Normal 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
68
internal_mail.go
Normal 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
3
kv.go
|
@ -1,3 +0,0 @@
|
||||||
package mailer
|
|
||||||
|
|
||||||
// kv interface (for unsubscribe links)
|
|
3
smtp.go
3
smtp.go
|
@ -1,3 +0,0 @@
|
||||||
package mailer
|
|
||||||
|
|
||||||
// smtp code
|
|
Loading…
Reference in a new issue