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