mirror of
https://github.com/ivabus/pop
synced 2024-12-04 22:15:08 +03:00
SMTP Support (#17)
This commit is contained in:
parent
db68f79f1d
commit
7397bcb80e
10 changed files with 289 additions and 50 deletions
26
README.md
26
README.md
|
@ -36,17 +36,39 @@ pop < message.md \
|
||||||
|
|
||||||
<img width="600" src="https://stuff.charm.sh/pop/resend-x-charm.png" alt="Resend and Charm logos">
|
<img width="600" src="https://stuff.charm.sh/pop/resend-x-charm.png" alt="Resend and Charm logos">
|
||||||
|
|
||||||
To use `pop`, you will need a `RESEND_API_KEY`.
|
To use `pop`, you will need a `RESEND_API_KEY` or configure an
|
||||||
|
[`SMTP`](#smtp-configuration) host.
|
||||||
|
|
||||||
You can grab one from: https://resend.com/api-keys.
|
You can grab one from: https://resend.com/api-keys.
|
||||||
|
|
||||||
|
### Resend Configuration
|
||||||
|
|
||||||
|
To use the resend delivery method, set the `RESEND_API_KEY` environment
|
||||||
|
variable.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export RESEND_API_KEY=$(pass RESEND_API_KEY)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### SMTP Configuration
|
||||||
|
|
||||||
|
To configure `pop` to use `SMTP`, you can set the following environment
|
||||||
|
variables.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export POP_SMTP_HOST=smtp.gmail.com
|
||||||
|
export POP_SMTP_PORT=587
|
||||||
|
export POP_SMTP_USERNAME=pop@charm.sh
|
||||||
|
export POP_SMTP_PASSWORD=hunter2
|
||||||
|
```
|
||||||
|
|
||||||
### Environment
|
### Environment
|
||||||
|
|
||||||
To avoid typing your `From: ` email address, you can also set the `POP_FROM`
|
To avoid typing your `From: ` email address, you can also set the `POP_FROM`
|
||||||
environment to pre-fill the field anytime you launch `pop`.
|
environment to pre-fill the field anytime you launch `pop`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export RESEND_API_KEY=$(pass RESEND_API_KEY)
|
|
||||||
export POP_FROM=pop@charm.sh
|
export POP_FROM=pop@charm.sh
|
||||||
export POP_SIGNATURE="Sent with [Pop](https://github.com/charmbracelet/pop)!"
|
export POP_SIGNATURE="Sent with [Pop](https://github.com/charmbracelet/pop)!"
|
||||||
```
|
```
|
||||||
|
|
|
@ -40,6 +40,6 @@ func (d attachmentDelegate) Render(w io.Writer, m list.Model, index int, item li
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d attachmentDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
|
func (d attachmentDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
100
email.go
100
email.go
|
@ -2,18 +2,23 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/resendlabs/resend-go"
|
"github.com/resendlabs/resend-go"
|
||||||
|
mail "github.com/xhit/go-simple-mail/v2"
|
||||||
"github.com/yuin/goldmark"
|
"github.com/yuin/goldmark"
|
||||||
"github.com/yuin/goldmark/extension"
|
"github.com/yuin/goldmark/extension"
|
||||||
renderer "github.com/yuin/goldmark/renderer/html"
|
renderer "github.com/yuin/goldmark/renderer/html"
|
||||||
)
|
)
|
||||||
|
|
||||||
const TO_SEPARATOR = ","
|
// ToSeparator is the separator used to split the To, Cc, and Bcc fields.
|
||||||
|
const ToSeparator = ","
|
||||||
|
|
||||||
// sendEmailSuccessMsg is the tea.Msg handled by Bubble Tea when the email has
|
// sendEmailSuccessMsg is the tea.Msg handled by Bubble Tea when the email has
|
||||||
// been sent successfully.
|
// been sent successfully.
|
||||||
|
@ -30,7 +35,18 @@ func (m Model) sendEmailCmd() tea.Cmd {
|
||||||
for i, a := range m.Attachments.Items() {
|
for i, a := range m.Attachments.Items() {
|
||||||
attachments[i] = a.FilterValue()
|
attachments[i] = a.FilterValue()
|
||||||
}
|
}
|
||||||
err := sendEmail(strings.Split(m.To.Value(), TO_SEPARATOR), m.From.Value(), m.Subject.Value(), m.Body.Value(), attachments)
|
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")
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sendEmailFailureMsg(err)
|
return sendEmailFailureMsg(err)
|
||||||
}
|
}
|
||||||
|
@ -38,8 +54,80 @@ func (m Model) sendEmailCmd() tea.Cmd {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendEmail(to []string, from, subject, body string, paths []string) error {
|
const gmailSuffix = "@gmail.com"
|
||||||
client := resend.NewClient(os.Getenv(RESEND_API_KEY))
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendResendEmail(to, cc, bcc []string, from, subject, body string, attachments []string) error {
|
||||||
|
client := resend.NewClient(resendAPIKey)
|
||||||
|
|
||||||
html := bytes.NewBufferString("")
|
html := bytes.NewBufferString("")
|
||||||
// If the conversion fails, we'll simply send the plain-text body.
|
// If the conversion fails, we'll simply send the plain-text body.
|
||||||
|
@ -62,10 +150,12 @@ func sendEmail(to []string, from, subject, body string, paths []string) error {
|
||||||
request := &resend.SendEmailRequest{
|
request := &resend.SendEmailRequest{
|
||||||
From: from,
|
From: from,
|
||||||
To: to,
|
To: to,
|
||||||
|
Cc: cc,
|
||||||
|
Bcc: bcc,
|
||||||
Subject: subject,
|
Subject: subject,
|
||||||
Html: html.String(),
|
Html: html.String(),
|
||||||
Text: body,
|
Text: body,
|
||||||
Attachments: makeAttachments(paths),
|
Attachments: makeAttachments(attachments),
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := client.Emails.Send(request)
|
_, err := client.Emails.Send(request)
|
||||||
|
|
6
go.mod
6
go.mod
|
@ -6,12 +6,13 @@ require (
|
||||||
github.com/charmbracelet/bubbles v0.16.2-0.20230711184233-0bdcc628fb8f
|
github.com/charmbracelet/bubbles v0.16.2-0.20230711184233-0bdcc628fb8f
|
||||||
github.com/charmbracelet/bubbletea v0.24.3-0.20230710130425-c4c83ba757f8
|
github.com/charmbracelet/bubbletea v0.24.3-0.20230710130425-c4c83ba757f8
|
||||||
github.com/charmbracelet/lipgloss v0.7.1
|
github.com/charmbracelet/lipgloss v0.7.1
|
||||||
|
github.com/charmbracelet/x/exp/ordered v0.0.0-20230707174939-50fb4f48b5b3
|
||||||
github.com/muesli/mango-cobra v1.2.0
|
github.com/muesli/mango-cobra v1.2.0
|
||||||
github.com/muesli/roff v0.1.0
|
github.com/muesli/roff v0.1.0
|
||||||
github.com/resendlabs/resend-go v1.7.0
|
github.com/resendlabs/resend-go v1.7.0
|
||||||
github.com/spf13/cobra v1.7.0
|
github.com/spf13/cobra v1.7.0
|
||||||
|
github.com/xhit/go-simple-mail/v2 v2.15.0
|
||||||
github.com/yuin/goldmark v1.5.5
|
github.com/yuin/goldmark v1.5.5
|
||||||
golang.org/x/exp v0.0.0-20230711153332-06a737ee72cb
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
@ -19,6 +20,7 @@ require (
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/go-test/deep v1.1.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
|
@ -33,6 +35,8 @@ require (
|
||||||
github.com/rivo/uniseg v0.4.4 // indirect
|
github.com/rivo/uniseg v0.4.4 // indirect
|
||||||
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect
|
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea // indirect
|
||||||
golang.org/x/sync v0.3.0 // indirect
|
golang.org/x/sync v0.3.0 // indirect
|
||||||
golang.org/x/sys v0.10.0 // indirect
|
golang.org/x/sys v0.10.0 // indirect
|
||||||
golang.org/x/term v0.10.0 // indirect
|
golang.org/x/term v0.10.0 // indirect
|
||||||
|
|
12
go.sum
12
go.sum
|
@ -8,12 +8,16 @@ github.com/charmbracelet/bubbletea v0.24.3-0.20230710130425-c4c83ba757f8 h1:rPWh
|
||||||
github.com/charmbracelet/bubbletea v0.24.3-0.20230710130425-c4c83ba757f8/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
|
github.com/charmbracelet/bubbletea v0.24.3-0.20230710130425-c4c83ba757f8/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
|
||||||
github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E=
|
github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E=
|
||||||
github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c=
|
github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c=
|
||||||
|
github.com/charmbracelet/x/exp/ordered v0.0.0-20230707174939-50fb4f48b5b3 h1:n1M8YLRcevMcCxr3vdLjtcrwVBwQpnbZU9IWvTxNhXw=
|
||||||
|
github.com/charmbracelet/x/exp/ordered v0.0.0-20230707174939-50fb4f48b5b3/go.mod h1:PHXDBVg6d66dpDTqESmefHTluiCgsCWPNtXA6g1ePGU=
|
||||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
|
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
|
||||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
|
||||||
|
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
@ -57,10 +61,14 @@ github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRM
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||||
|
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM=
|
||||||
|
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
||||||
|
github.com/xhit/go-simple-mail/v2 v2.15.0 h1:qMXeqcZErUW/Dw6EXxmPuxHzVI8MdxWnEnu2xcisohU=
|
||||||
|
github.com/xhit/go-simple-mail/v2 v2.15.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
|
||||||
github.com/yuin/goldmark v1.5.5 h1:IJznPe8wOzfIKETmMkd06F8nXkmlhaHqFRM9l1hAGsU=
|
github.com/yuin/goldmark v1.5.5 h1:IJznPe8wOzfIKETmMkd06F8nXkmlhaHqFRM9l1hAGsU=
|
||||||
github.com/yuin/goldmark v1.5.5/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.5.5/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
golang.org/x/exp v0.0.0-20230711153332-06a737ee72cb h1:xIApU0ow1zwMa2uL1VDNeQlNVFTWMQxZUZCMDy0Q4Us=
|
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4=
|
||||||
golang.org/x/exp v0.0.0-20230711153332-06a737ee72cb/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|
21
go.work.sum
21
go.work.sum
|
@ -1 +1,20 @@
|
||||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM=
|
||||||
|
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
||||||
|
github.com/xhit/go-simple-mail/v2 v2.15.0 h1:qMXeqcZErUW/Dw6EXxmPuxHzVI8MdxWnEnu2xcisohU=
|
||||||
|
github.com/xhit/go-simple-mail/v2 v2.15.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
|
||||||
|
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/yuin/goldmark v1.5.5 h1:IJznPe8wOzfIKETmMkd06F8nXkmlhaHqFRM9l1hAGsU=
|
||||||
|
github.com/yuin/goldmark v1.5.5/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
golang.org/x/exp v0.0.0-20230711153332-06a737ee72cb h1:xIApU0ow1zwMa2uL1VDNeQlNVFTWMQxZUZCMDy0Q4Us=
|
||||||
|
golang.org/x/exp v0.0.0-20230711153332-06a737ee72cb/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||||
|
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||||
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||||
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
|
||||||
|
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||||
|
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
@ -13,6 +13,7 @@ type KeyMap struct {
|
||||||
Quit key.Binding
|
Quit key.Binding
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultKeybinds returns the default key bindings for the application.
|
||||||
func DefaultKeybinds() KeyMap {
|
func DefaultKeybinds() KeyMap {
|
||||||
return KeyMap{
|
return KeyMap{
|
||||||
NextInput: key.NewBinding(
|
NextInput: key.NewBinding(
|
||||||
|
@ -49,6 +50,7 @@ func DefaultKeybinds() KeyMap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ShortHelp returns the key bindings for the short help screen.
|
||||||
func (k KeyMap) ShortHelp() []key.Binding {
|
func (k KeyMap) ShortHelp() []key.Binding {
|
||||||
return []key.Binding{
|
return []key.Binding{
|
||||||
k.NextInput,
|
k.NextInput,
|
||||||
|
@ -59,6 +61,7 @@ func (k KeyMap) ShortHelp() []key.Binding {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FullHelp returns the key bindings for the full help screen.
|
||||||
func (k KeyMap) FullHelp() [][]key.Binding {
|
func (k KeyMap) FullHelp() [][]key.Binding {
|
||||||
return [][]key.Binding{
|
return [][]key.Binding{
|
||||||
{k.NextInput, k.Send, k.Attach, k.Unattach, k.Quit},
|
{k.NextInput, k.Send, k.Attach, k.Unattach, k.Quit},
|
||||||
|
|
111
main.go
111
main.go
|
@ -5,6 +5,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
@ -14,20 +15,57 @@ import (
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
const RESEND_API_KEY = "RESEND_API_KEY"
|
// PopUnsafeHTML is the environment variable that enables unsafe HTML in the
|
||||||
const UNSAFE_HTML = "POP_UNSAFE_HTML"
|
// email body.
|
||||||
const POP_FROM = "POP_FROM"
|
const PopUnsafeHTML = "POP_UNSAFE_HTML"
|
||||||
const POP_SIGNATURE = "POP_SIGNATURE"
|
|
||||||
|
// ResendAPIKey is the environment variable that enables Resend as a delivery
|
||||||
|
// method and uses it to send the email.
|
||||||
|
const ResendAPIKey = "RESEND_API_KEY" //nolint:gosec
|
||||||
|
|
||||||
|
// PopFrom is the environment variable that sets the default "from" address.
|
||||||
|
const PopFrom = "POP_FROM"
|
||||||
|
|
||||||
|
// PopSignature is the environment variable that sets the default signature.
|
||||||
|
const PopSignature = "POP_SIGNATURE"
|
||||||
|
|
||||||
|
// PopSMTPHost is the host for the SMTP server if the user is using the SMTP delivery method.
|
||||||
|
const PopSMTPHost = "POP_SMTP_HOST"
|
||||||
|
|
||||||
|
// PopSMTPPort is the port for the SMTP server if the user is using the SMTP delivery method.
|
||||||
|
const PopSMTPPort = "POP_SMTP_PORT"
|
||||||
|
|
||||||
|
// PopSMTPUsername is the username for the SMTP server if the user is using the SMTP delivery method.
|
||||||
|
const PopSMTPUsername = "POP_SMTP_USERNAME"
|
||||||
|
|
||||||
|
// PopSMTPPassword is the password for the SMTP server if the user is using the SMTP delivery method.
|
||||||
|
const PopSMTPPassword = "POP_SMTP_PASSWORD" //nolint:gosec
|
||||||
|
|
||||||
|
// PopSMTPEncryption is the encryption type for the SMTP server if the user is using the SMTP delivery method.
|
||||||
|
const PopSMTPEncryption = "POP_SMTP_ENCRYPTION" //nolint:gosec
|
||||||
|
|
||||||
|
// PopSMTPInsecureSkipVerify is whether or not to skip TLS verification for the
|
||||||
|
// SMTP server if the user is using the SMTP delivery method.
|
||||||
|
const PopSMTPInsecureSkipVerify = "POP_SMTP_INSECURE_SKIP_VERIFY"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
from string
|
from string
|
||||||
to []string
|
to []string
|
||||||
|
cc []string
|
||||||
|
bcc []string
|
||||||
subject string
|
subject string
|
||||||
body string
|
body string
|
||||||
attachments []string
|
attachments []string
|
||||||
preview bool
|
preview bool
|
||||||
unsafe bool
|
unsafe bool
|
||||||
signature string
|
signature string
|
||||||
|
smtpHost string
|
||||||
|
smtpPort int
|
||||||
|
smtpUsername string
|
||||||
|
smtpPassword string
|
||||||
|
smtpEncryption string
|
||||||
|
smtpInsecureSkipVerify bool
|
||||||
|
resendAPIKey string
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
|
@ -35,10 +73,19 @@ var rootCmd = &cobra.Command{
|
||||||
Short: "Send emails from your terminal",
|
Short: "Send emails from your terminal",
|
||||||
Long: `Pop is a tool for sending emails from your terminal.`,
|
Long: `Pop is a tool for sending emails from your terminal.`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if os.Getenv(RESEND_API_KEY) == "" {
|
var deliveryMethod DeliveryMethod
|
||||||
fmt.Printf("\n %s %s %s\n\n", errorHeaderStyle.String(), inlineCodeStyle.Render(RESEND_API_KEY), "environment variable is required.")
|
switch {
|
||||||
|
case resendAPIKey != "":
|
||||||
|
deliveryMethod = Resend
|
||||||
|
case smtpUsername != "" && smtpPassword != "":
|
||||||
|
deliveryMethod = SMTP
|
||||||
|
from = smtpUsername
|
||||||
|
}
|
||||||
|
|
||||||
|
if deliveryMethod == None {
|
||||||
|
fmt.Printf("\n %s %s %s\n\n", errorHeaderStyle.String(), inlineCodeStyle.Render(ResendAPIKey), "environment variable is required.")
|
||||||
fmt.Printf(" %s %s\n\n", commentStyle.Render("You can grab one at"), linkStyle.Render("https://resend.com/api-keys"))
|
fmt.Printf(" %s %s\n\n", commentStyle.Render("You can grab one at"), linkStyle.Render("https://resend.com/api-keys"))
|
||||||
os.Exit(1)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasStdin() {
|
if hasStdin() {
|
||||||
|
@ -54,7 +101,15 @@ var rootCmd = &cobra.Command{
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(to) > 0 && from != "" && subject != "" && body != "" && !preview {
|
if len(to) > 0 && from != "" && subject != "" && body != "" && !preview {
|
||||||
err := sendEmail(to, from, subject, body, attachments)
|
var err error
|
||||||
|
switch deliveryMethod {
|
||||||
|
case SMTP:
|
||||||
|
err = sendSMTPEmail(to, cc, bcc, from, subject, body, attachments)
|
||||||
|
case Resend:
|
||||||
|
err = sendResendEmail(to, cc, bcc, from, subject, body, attachments)
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unknown delivery method")
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cmd.SilenceUsage = true
|
cmd.SilenceUsage = true
|
||||||
cmd.SilenceErrors = true
|
cmd.SilenceErrors = true
|
||||||
|
@ -71,14 +126,15 @@ var rootCmd = &cobra.Command{
|
||||||
Subject: subject,
|
Subject: subject,
|
||||||
Text: body,
|
Text: body,
|
||||||
Attachments: makeAttachments(attachments),
|
Attachments: makeAttachments(attachments),
|
||||||
}))
|
}, deliveryMethod))
|
||||||
|
|
||||||
m, err := p.Run()
|
m, err := p.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
mm := m.(Model)
|
mm := m.(Model)
|
||||||
if !mm.abort {
|
if !mm.abort {
|
||||||
fmt.Print(emailSummary(strings.Split(mm.To.Value(), TO_SEPARATOR), mm.Subject.Value()))
|
fmt.Print(emailSummary(strings.Split(mm.To.Value(), ToSeparator), mm.Subject.Value()))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
@ -101,6 +157,7 @@ var (
|
||||||
CommitSHA string
|
CommitSHA string
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CompletionCmd is the cobra command for generating completion scripts.
|
||||||
var CompletionCmd = &cobra.Command{
|
var CompletionCmd = &cobra.Command{
|
||||||
Use: "completion [bash|zsh|fish|powershell]",
|
Use: "completion [bash|zsh|fish|powershell]",
|
||||||
Short: "Generate completion script",
|
Short: "Generate completion script",
|
||||||
|
@ -123,6 +180,7 @@ var CompletionCmd = &cobra.Command{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ManCmd is the cobra command for the manual.
|
||||||
var ManCmd = &cobra.Command{
|
var ManCmd = &cobra.Command{
|
||||||
Use: "man",
|
Use: "man",
|
||||||
Short: "Generate man page",
|
Short: "Generate man page",
|
||||||
|
@ -144,19 +202,36 @@ var ManCmd = &cobra.Command{
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(CompletionCmd, ManCmd)
|
rootCmd.AddCommand(CompletionCmd, ManCmd)
|
||||||
|
|
||||||
rootCmd.Flags().StringSliceVar(&to, "bcc", []string{}, "BCC recipients")
|
rootCmd.Flags().StringSliceVar(&bcc, "bcc", []string{}, "BCC recipients")
|
||||||
rootCmd.Flags().StringSliceVar(&to, "cc", []string{}, "CC recipients")
|
rootCmd.Flags().StringSliceVar(&cc, "cc", []string{}, "CC recipients")
|
||||||
rootCmd.Flags().StringSliceVarP(&attachments, "attach", "a", []string{}, "Email's attachments")
|
rootCmd.Flags().StringSliceVarP(&attachments, "attach", "a", []string{}, "Email's attachments")
|
||||||
rootCmd.Flags().StringSliceVarP(&to, "to", "t", []string{}, "Recipients")
|
rootCmd.Flags().StringSliceVarP(&to, "to", "t", []string{}, "Recipients")
|
||||||
rootCmd.Flags().StringVarP(&body, "body", "b", "", "Email's contents")
|
rootCmd.Flags().StringVarP(&body, "body", "b", "", "Email's contents")
|
||||||
envFrom := os.Getenv(POP_FROM)
|
envFrom := os.Getenv(PopFrom)
|
||||||
rootCmd.Flags().StringVarP(&from, "from", "f", envFrom, "Email's sender "+commentStyle.Render("($"+POP_FROM+")"))
|
rootCmd.Flags().StringVarP(&from, "from", "f", envFrom, "Email's sender"+commentStyle.Render("($"+PopFrom+")"))
|
||||||
rootCmd.Flags().StringVarP(&subject, "subject", "s", "", "Email's subject")
|
rootCmd.Flags().StringVarP(&subject, "subject", "s", "", "Email's subject")
|
||||||
rootCmd.Flags().BoolVarP(&preview, "preview", "p", false, "Whether to preview the email before sending")
|
rootCmd.Flags().BoolVar(&preview, "preview", false, "Whether to preview the email before sending")
|
||||||
envUnsafe := os.Getenv(UNSAFE_HTML) == "true"
|
envUnsafe := os.Getenv(PopUnsafeHTML) == "true"
|
||||||
rootCmd.Flags().BoolVarP(&unsafe, "unsafe", "u", envUnsafe, "Whether to allow unsafe HTML in the email body, also enable some extra markdown features (Experimental)")
|
rootCmd.Flags().BoolVarP(&unsafe, "unsafe", "u", envUnsafe, "Whether to allow unsafe HTML in the email body, also enable some extra markdown features (Experimental)")
|
||||||
envSignature := os.Getenv("POP_SIGNATURE")
|
envSignature := os.Getenv(PopSignature)
|
||||||
rootCmd.Flags().StringVarP(&signature, "signature", "x", envSignature, "Signature to display at the end of the email. "+commentStyle.Render("($"+POP_SIGNATURE+")"))
|
rootCmd.Flags().StringVarP(&signature, "signature", "x", envSignature, "Signature to display at the end of the email."+commentStyle.Render("($"+PopSignature+")"))
|
||||||
|
envSMTPHost := os.Getenv(PopSMTPHost)
|
||||||
|
rootCmd.Flags().StringVarP(&smtpHost, "smtp.host", "H", envSMTPHost, "Host of the SMTP server"+commentStyle.Render("($"+PopSMTPHost+")"))
|
||||||
|
envSMTPPort, _ := strconv.Atoi(os.Getenv(PopSMTPPort))
|
||||||
|
if envSMTPPort == 0 {
|
||||||
|
envSMTPPort = 587
|
||||||
|
}
|
||||||
|
rootCmd.Flags().IntVarP(&smtpPort, "smtp.port", "P", envSMTPPort, "Port of the SMTP server"+commentStyle.Render("($"+PopSMTPPort+")"))
|
||||||
|
envSMTPUsername := os.Getenv(PopSMTPUsername)
|
||||||
|
rootCmd.Flags().StringVarP(&smtpUsername, "smtp.username", "U", envSMTPUsername, "Username of the SMTP server"+commentStyle.Render("($"+PopSMTPUsername+")"))
|
||||||
|
envSMTPPassword := os.Getenv(PopSMTPPassword)
|
||||||
|
rootCmd.Flags().StringVarP(&smtpPassword, "smtp.password", "p", envSMTPPassword, "Password of the SMTP server"+commentStyle.Render("($"+PopSMTPPassword+")"))
|
||||||
|
envSMTPEncryption := os.Getenv(PopSMTPEncryption)
|
||||||
|
rootCmd.Flags().StringVarP(&smtpEncryption, "smtp.encryption", "e", envSMTPEncryption, "Encryption type of the SMTP server (starttls, ssl, or none)"+commentStyle.Render("($"+PopSMTPEncryption+")"))
|
||||||
|
envInsecureSkipVerify := os.Getenv(PopSMTPInsecureSkipVerify) == "true"
|
||||||
|
rootCmd.Flags().BoolVarP(&smtpInsecureSkipVerify, "smtp.insecure", "i", envInsecureSkipVerify, "Skip TLS verification with SMTP server"+commentStyle.Render("($"+PopSMTPInsecureSkipVerify+")"))
|
||||||
|
envResendAPIKey := os.Getenv(ResendAPIKey)
|
||||||
|
rootCmd.Flags().StringVarP(&resendAPIKey, "resend.key", "r", envResendAPIKey, "API key for the Resend.com"+commentStyle.Render("($"+ResendAPIKey+")"))
|
||||||
|
|
||||||
rootCmd.CompletionOptions.HiddenDefaultCmd = true
|
rootCmd.CompletionOptions.HiddenDefaultCmd = true
|
||||||
|
|
||||||
|
|
40
model.go
40
model.go
|
@ -13,10 +13,11 @@ import (
|
||||||
"github.com/charmbracelet/bubbles/textarea"
|
"github.com/charmbracelet/bubbles/textarea"
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/x/exp/ordered"
|
||||||
"github.com/resendlabs/resend-go"
|
"github.com/resendlabs/resend-go"
|
||||||
"golang.org/x/exp/constraints"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// State is the current state of the application.
|
||||||
type State int
|
type State int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -30,10 +31,26 @@ const (
|
||||||
sendingEmail
|
sendingEmail
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DeliveryMethod is the method of delivery for the email.
|
||||||
|
type DeliveryMethod int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// None is the default delivery method.
|
||||||
|
None DeliveryMethod = iota
|
||||||
|
// Resend uses https://resend.com to send an email.
|
||||||
|
Resend
|
||||||
|
// SMTP uses an SMTP server to send an email.
|
||||||
|
SMTP
|
||||||
|
)
|
||||||
|
|
||||||
|
// Model is Pop's application model.
|
||||||
type Model struct {
|
type Model struct {
|
||||||
// state represents the current state of the application.
|
// state represents the current state of the application.
|
||||||
state State
|
state State
|
||||||
|
|
||||||
|
// DeliveryMethod is whether we are using DeliveryMethod or Resend.
|
||||||
|
DeliveryMethod DeliveryMethod
|
||||||
|
|
||||||
// From represents the sender's email address.
|
// From represents the sender's email address.
|
||||||
From textinput.Model
|
From textinput.Model
|
||||||
// To represents the recipient's email address.
|
// To represents the recipient's email address.
|
||||||
|
@ -48,6 +65,9 @@ type Model struct {
|
||||||
// This is a list of file paths which are picked with a filepicker.
|
// This is a list of file paths which are picked with a filepicker.
|
||||||
Attachments list.Model
|
Attachments list.Model
|
||||||
|
|
||||||
|
Cc textinput.Model
|
||||||
|
Bcc textinput.Model
|
||||||
|
|
||||||
// filepicker is used to pick file attachments.
|
// filepicker is used to pick file attachments.
|
||||||
filepicker filepicker.Model
|
filepicker filepicker.Model
|
||||||
loadingSpinner spinner.Model
|
loadingSpinner spinner.Model
|
||||||
|
@ -58,7 +78,8 @@ type Model struct {
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModel(defaults resend.SendEmailRequest) Model {
|
// NewModel returns a new model for the application.
|
||||||
|
func NewModel(defaults resend.SendEmailRequest, deliveryMethod DeliveryMethod) Model {
|
||||||
from := textinput.New()
|
from := textinput.New()
|
||||||
from.Prompt = "From "
|
from.Prompt = "From "
|
||||||
from.Placeholder = "me@example.com"
|
from.Placeholder = "me@example.com"
|
||||||
|
@ -76,7 +97,7 @@ func NewModel(defaults resend.SendEmailRequest) Model {
|
||||||
to.PlaceholderStyle = placeholderStyle
|
to.PlaceholderStyle = placeholderStyle
|
||||||
to.TextStyle = textStyle
|
to.TextStyle = textStyle
|
||||||
to.Placeholder = "you@example.com"
|
to.Placeholder = "you@example.com"
|
||||||
to.SetValue(strings.Join(defaults.To, TO_SEPARATOR))
|
to.SetValue(strings.Join(defaults.To, ToSeparator))
|
||||||
|
|
||||||
subject := textinput.New()
|
subject := textinput.New()
|
||||||
subject.Prompt = "Subject "
|
subject.Prompt = "Subject "
|
||||||
|
@ -155,6 +176,7 @@ func NewModel(defaults resend.SendEmailRequest) Model {
|
||||||
help: help.New(),
|
help: help.New(),
|
||||||
keymap: DefaultKeybinds(),
|
keymap: DefaultKeybinds(),
|
||||||
loadingSpinner: loadingSpinner,
|
loadingSpinner: loadingSpinner,
|
||||||
|
DeliveryMethod: deliveryMethod,
|
||||||
}
|
}
|
||||||
|
|
||||||
m.focusActiveInput()
|
m.focusActiveInput()
|
||||||
|
@ -162,6 +184,7 @@ func NewModel(defaults resend.SendEmailRequest) Model {
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Init initializes the model.
|
||||||
func (m Model) Init() tea.Cmd {
|
func (m Model) Init() tea.Cmd {
|
||||||
return tea.Batch(
|
return tea.Batch(
|
||||||
m.From.Cursor.BlinkCmd(),
|
m.From.Cursor.BlinkCmd(),
|
||||||
|
@ -176,6 +199,7 @@ func clearErrAfter(d time.Duration) tea.Cmd {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update is the update loop for the model.
|
||||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case sendEmailSuccessMsg:
|
case sendEmailSuccessMsg:
|
||||||
|
@ -243,7 +267,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m, m.filepicker.Init()
|
return m, m.filepicker.Init()
|
||||||
case key.Matches(msg, m.keymap.Unattach):
|
case key.Matches(msg, m.keymap.Unattach):
|
||||||
m.Attachments.RemoveItem(m.Attachments.Index())
|
m.Attachments.RemoveItem(m.Attachments.Index())
|
||||||
m.Attachments.SetHeight(max(len(m.Attachments.Items()), 1) + 2)
|
m.Attachments.SetHeight(ordered.Max(len(m.Attachments.Items()), 1) + 2)
|
||||||
case key.Matches(msg, m.keymap.Quit):
|
case key.Matches(msg, m.keymap.Quit):
|
||||||
m.quitting = true
|
m.quitting = true
|
||||||
m.abort = true
|
m.abort = true
|
||||||
|
@ -329,6 +353,7 @@ func (m *Model) focusActiveInput() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// View displays the application.
|
||||||
func (m Model) View() string {
|
func (m Model) View() string {
|
||||||
if m.quitting {
|
if m.quitting {
|
||||||
return ""
|
return ""
|
||||||
|
@ -371,10 +396,3 @@ func (m Model) View() string {
|
||||||
|
|
||||||
return paddedStyle.Render(s.String())
|
return paddedStyle.Render(s.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func max[T constraints.Ordered](a, b T) T {
|
|
||||||
if a > b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
2
style.go
2
style.go
|
@ -27,7 +27,7 @@ var (
|
||||||
|
|
||||||
errorHeaderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#F1F1F1")).Background(lipgloss.Color("#FF5F87")).Bold(true).Padding(0, 1).SetString("ERROR")
|
errorHeaderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#F1F1F1")).Background(lipgloss.Color("#FF5F87")).Bold(true).Padding(0, 1).SetString("ERROR")
|
||||||
errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5F87"))
|
errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5F87"))
|
||||||
commentStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#757575"))
|
commentStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#757575")).PaddingLeft(1)
|
||||||
|
|
||||||
sendButtonActiveStyle = lipgloss.NewStyle().Background(accentColor).Foreground(yellowColor).Padding(0, 2)
|
sendButtonActiveStyle = lipgloss.NewStyle().Background(accentColor).Foreground(yellowColor).Padding(0, 2)
|
||||||
sendButtonInactiveStyle = lipgloss.NewStyle().Background(darkGrayColor).Foreground(lightGrayColor).Padding(0, 2)
|
sendButtonInactiveStyle = lipgloss.NewStyle().Background(darkGrayColor).Foreground(lightGrayColor).Padding(0, 2)
|
||||||
|
|
Loading…
Reference in a new issue