2023-06-15 16:45:14 +03:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2023-06-15 17:30:20 +03:00
|
|
|
"bytes"
|
2023-07-31 17:32:02 +03:00
|
|
|
"crypto/tls"
|
|
|
|
"errors"
|
2023-06-15 16:45:14 +03:00
|
|
|
"os"
|
2023-06-15 19:02:09 +03:00
|
|
|
"path/filepath"
|
2023-06-15 17:30:20 +03:00
|
|
|
"strings"
|
2023-07-31 17:32:02 +03:00
|
|
|
"time"
|
2023-06-15 16:45:14 +03:00
|
|
|
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
|
|
"github.com/resendlabs/resend-go"
|
2023-07-31 17:32:02 +03:00
|
|
|
mail "github.com/xhit/go-simple-mail/v2"
|
2023-06-15 17:30:20 +03:00
|
|
|
"github.com/yuin/goldmark"
|
2023-07-20 23:42:37 +03:00
|
|
|
"github.com/yuin/goldmark/extension"
|
|
|
|
renderer "github.com/yuin/goldmark/renderer/html"
|
2023-06-15 16:45:14 +03:00
|
|
|
)
|
|
|
|
|
2023-07-31 17:32:02 +03:00
|
|
|
// ToSeparator is the separator used to split the To, Cc, and Bcc fields.
|
|
|
|
const ToSeparator = ","
|
2023-06-15 17:30:20 +03:00
|
|
|
|
2023-06-15 16:45:14 +03:00
|
|
|
// sendEmailSuccessMsg is the tea.Msg handled by Bubble Tea when the email has
|
|
|
|
// been sent successfully.
|
|
|
|
type sendEmailSuccessMsg struct{}
|
|
|
|
|
|
|
|
// sendEmailFailureMsg is the tea.Msg handled by Bubble Tea when the email has
|
|
|
|
// failed to send.
|
|
|
|
type sendEmailFailureMsg error
|
|
|
|
|
|
|
|
// sendEmailCmd returns a tea.Cmd that sends the email.
|
|
|
|
func (m Model) sendEmailCmd() tea.Cmd {
|
|
|
|
return func() tea.Msg {
|
|
|
|
attachments := make([]string, len(m.Attachments.Items()))
|
|
|
|
for i, a := range m.Attachments.Items() {
|
2023-06-15 19:05:07 +03:00
|
|
|
attachments[i] = a.FilterValue()
|
2023-06-15 16:45:14 +03:00
|
|
|
}
|
2023-07-31 17:32:02 +03:00
|
|
|
var err error
|
|
|
|
to := strings.Split(m.To.Value(), ToSeparator)
|
|
|
|
cc := strings.Split(m.Cc.Value(), ToSeparator)
|
|
|
|
bcc := strings.Split(m.Bcc.Value(), ToSeparator)
|
|
|
|
switch m.DeliveryMethod {
|
|
|
|
case SMTP:
|
|
|
|
err = sendSMTPEmail(to, cc, bcc, m.From.Value(), m.Subject.Value(), m.Body.Value(), attachments)
|
|
|
|
case Resend:
|
|
|
|
err = sendResendEmail(to, cc, bcc, m.From.Value(), m.Subject.Value(), m.Body.Value(), attachments)
|
|
|
|
default:
|
|
|
|
err = errors.New("[ERROR]: unknown delivery method")
|
|
|
|
}
|
2023-06-15 16:45:14 +03:00
|
|
|
if err != nil {
|
|
|
|
return sendEmailFailureMsg(err)
|
|
|
|
}
|
|
|
|
return sendEmailSuccessMsg{}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-31 17:32:02 +03:00
|
|
|
const gmailSuffix = "@gmail.com"
|
|
|
|
const gmailSMTPHost = "smtp.gmail.com"
|
|
|
|
const gmailSMTPPort = 587
|
|
|
|
|
|
|
|
func sendSMTPEmail(to, cc, bcc []string, from, subject, body string, attachments []string) error {
|
|
|
|
server := mail.NewSMTPClient()
|
|
|
|
|
|
|
|
var err error
|
|
|
|
server.Username = smtpUsername
|
|
|
|
server.Password = smtpPassword
|
|
|
|
server.Host = smtpHost
|
|
|
|
server.Port = smtpPort
|
|
|
|
|
|
|
|
// Set defaults for gmail.
|
|
|
|
if strings.HasSuffix(server.Username, gmailSuffix) {
|
|
|
|
if server.Port == 0 {
|
|
|
|
server.Port = gmailSMTPPort
|
|
|
|
}
|
|
|
|
if server.Host == "" {
|
|
|
|
server.Host = gmailSMTPHost
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
switch strings.ToLower(smtpEncryption) {
|
|
|
|
case "ssl":
|
|
|
|
server.Encryption = mail.EncryptionSSLTLS
|
|
|
|
case "none":
|
|
|
|
server.Encryption = mail.EncryptionNone
|
|
|
|
default:
|
|
|
|
server.Encryption = mail.EncryptionSTARTTLS
|
|
|
|
}
|
|
|
|
|
|
|
|
server.KeepAlive = false
|
|
|
|
server.ConnectTimeout = 10 * time.Second
|
|
|
|
server.SendTimeout = 10 * time.Second
|
|
|
|
server.TLSConfig = &tls.Config{
|
|
|
|
InsecureSkipVerify: smtpInsecureSkipVerify, //nolint:gosec
|
|
|
|
ServerName: server.Host,
|
|
|
|
}
|
|
|
|
|
|
|
|
smtpClient, err := server.Connect()
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
email := mail.NewMSG()
|
|
|
|
email.SetFrom(from).
|
|
|
|
AddTo(to...).
|
|
|
|
AddCc(cc...).
|
|
|
|
AddBcc(bcc...).
|
|
|
|
SetSubject(subject)
|
|
|
|
|
|
|
|
html := bytes.NewBufferString("")
|
|
|
|
convertErr := goldmark.Convert([]byte(body), html)
|
|
|
|
|
|
|
|
if convertErr != nil {
|
|
|
|
email.SetBody(mail.TextPlain, body)
|
|
|
|
} else {
|
|
|
|
email.SetBody(mail.TextHTML, html.String())
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, a := range attachments {
|
|
|
|
email.Attach(&mail.File{
|
|
|
|
FilePath: a,
|
|
|
|
Name: filepath.Base(a),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return email.Send(smtpClient)
|
|
|
|
}
|
|
|
|
|
2023-08-01 15:32:28 +03:00
|
|
|
func sendResendEmail(to, _, _ []string, from, subject, body string, attachments []string) error {
|
2023-07-31 17:32:02 +03:00
|
|
|
client := resend.NewClient(resendAPIKey)
|
2023-06-15 16:45:14 +03:00
|
|
|
|
2023-06-21 21:44:39 +03:00
|
|
|
html := bytes.NewBufferString("")
|
|
|
|
// If the conversion fails, we'll simply send the plain-text body.
|
2023-07-20 23:42:37 +03:00
|
|
|
if unsafe {
|
|
|
|
markdown := goldmark.New(
|
|
|
|
goldmark.WithRendererOptions(
|
|
|
|
renderer.WithUnsafe(),
|
|
|
|
),
|
|
|
|
goldmark.WithExtensions(
|
|
|
|
extension.Strikethrough,
|
|
|
|
extension.Table,
|
|
|
|
extension.Linkify,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
_ = markdown.Convert([]byte(body), html)
|
|
|
|
} else {
|
|
|
|
_ = goldmark.Convert([]byte(body), html)
|
|
|
|
}
|
2023-06-15 17:30:20 +03:00
|
|
|
|
2023-06-15 16:45:14 +03:00
|
|
|
request := &resend.SendEmailRequest{
|
2023-06-15 19:02:09 +03:00
|
|
|
From: from,
|
|
|
|
To: to,
|
|
|
|
Subject: subject,
|
|
|
|
Html: html.String(),
|
2023-06-21 21:44:39 +03:00
|
|
|
Text: body,
|
2023-07-31 17:32:02 +03:00
|
|
|
Attachments: makeAttachments(attachments),
|
2023-06-15 16:45:14 +03:00
|
|
|
}
|
|
|
|
|
2023-06-21 21:44:39 +03:00
|
|
|
_, err := client.Emails.Send(request)
|
2023-06-15 16:45:14 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2023-06-27 21:34:28 +03:00
|
|
|
|
|
|
|
func makeAttachments(paths []string) []resend.Attachment {
|
|
|
|
if len(paths) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
attachments := make([]resend.Attachment, len(paths))
|
|
|
|
for i, a := range paths {
|
|
|
|
f, err := os.ReadFile(a)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
attachments[i] = resend.Attachment{
|
|
|
|
Content: string(f),
|
|
|
|
Filename: filepath.Base(a),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return attachments
|
|
|
|
}
|