mirror of
https://github.com/ivabus/pop
synced 2024-12-04 14:05:09 +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">
|
||||
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
||||
To avoid typing your `From: ` email address, you can also set the `POP_FROM`
|
||||
environment to pre-fill the field anytime you launch `pop`.
|
||||
|
||||
```bash
|
||||
export RESEND_API_KEY=$(pass RESEND_API_KEY)
|
||||
export POP_FROM=pop@charm.sh
|
||||
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
|
||||
}
|
||||
|
|
100
email.go
100
email.go
|
@ -2,18 +2,23 @@ package main
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/resendlabs/resend-go"
|
||||
mail "github.com/xhit/go-simple-mail/v2"
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
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
|
||||
// been sent successfully.
|
||||
|
@ -30,7 +35,18 @@ func (m Model) sendEmailCmd() tea.Cmd {
|
|||
for i, a := range m.Attachments.Items() {
|
||||
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 {
|
||||
return sendEmailFailureMsg(err)
|
||||
}
|
||||
|
@ -38,8 +54,80 @@ func (m Model) sendEmailCmd() tea.Cmd {
|
|||
}
|
||||
}
|
||||
|
||||
func sendEmail(to []string, from, subject, body string, paths []string) error {
|
||||
client := resend.NewClient(os.Getenv(RESEND_API_KEY))
|
||||
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)
|
||||
}
|
||||
|
||||
func sendResendEmail(to, cc, bcc []string, from, subject, body string, attachments []string) error {
|
||||
client := resend.NewClient(resendAPIKey)
|
||||
|
||||
html := bytes.NewBufferString("")
|
||||
// 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{
|
||||
From: from,
|
||||
To: to,
|
||||
Cc: cc,
|
||||
Bcc: bcc,
|
||||
Subject: subject,
|
||||
Html: html.String(),
|
||||
Text: body,
|
||||
Attachments: makeAttachments(paths),
|
||||
Attachments: makeAttachments(attachments),
|
||||
}
|
||||
|
||||
_, 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/bubbletea v0.24.3-0.20230710130425-c4c83ba757f8
|
||||
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/roff v0.1.0
|
||||
github.com/resendlabs/resend-go 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
|
||||
golang.org/x/exp v0.0.0-20230711153332-06a737ee72cb
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -19,6 +20,7 @@ require (
|
|||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // 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/lucasb-eyer/go-colorful v1.2.0 // 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/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // 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/sys 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/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E=
|
||||
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/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||
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/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/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/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
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/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
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/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/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4=
|
||||
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/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
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
|
||||
}
|
||||
|
||||
// DefaultKeybinds returns the default key bindings for the application.
|
||||
func DefaultKeybinds() KeyMap {
|
||||
return KeyMap{
|
||||
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 {
|
||||
return []key.Binding{
|
||||
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 {
|
||||
return [][]key.Binding{
|
||||
{k.NextInput, k.Send, k.Attach, k.Unattach, k.Quit},
|
||||
|
|
127
main.go
127
main.go
|
@ -5,6 +5,7 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
@ -14,20 +15,57 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const RESEND_API_KEY = "RESEND_API_KEY"
|
||||
const UNSAFE_HTML = "POP_UNSAFE_HTML"
|
||||
const POP_FROM = "POP_FROM"
|
||||
const POP_SIGNATURE = "POP_SIGNATURE"
|
||||
// PopUnsafeHTML is the environment variable that enables unsafe HTML in the
|
||||
// email body.
|
||||
const PopUnsafeHTML = "POP_UNSAFE_HTML"
|
||||
|
||||
// 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 (
|
||||
from string
|
||||
to []string
|
||||
subject string
|
||||
body string
|
||||
attachments []string
|
||||
preview bool
|
||||
unsafe bool
|
||||
signature string
|
||||
from string
|
||||
to []string
|
||||
cc []string
|
||||
bcc []string
|
||||
subject string
|
||||
body string
|
||||
attachments []string
|
||||
preview bool
|
||||
unsafe bool
|
||||
signature string
|
||||
smtpHost string
|
||||
smtpPort int
|
||||
smtpUsername string
|
||||
smtpPassword string
|
||||
smtpEncryption string
|
||||
smtpInsecureSkipVerify bool
|
||||
resendAPIKey string
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
|
@ -35,10 +73,19 @@ var rootCmd = &cobra.Command{
|
|||
Short: "Send emails from your terminal",
|
||||
Long: `Pop is a tool for sending emails from your terminal.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if os.Getenv(RESEND_API_KEY) == "" {
|
||||
fmt.Printf("\n %s %s %s\n\n", errorHeaderStyle.String(), inlineCodeStyle.Render(RESEND_API_KEY), "environment variable is required.")
|
||||
var deliveryMethod DeliveryMethod
|
||||
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"))
|
||||
os.Exit(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
if hasStdin() {
|
||||
|
@ -54,7 +101,15 @@ var rootCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
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 {
|
||||
cmd.SilenceUsage = true
|
||||
cmd.SilenceErrors = true
|
||||
|
@ -71,14 +126,15 @@ var rootCmd = &cobra.Command{
|
|||
Subject: subject,
|
||||
Text: body,
|
||||
Attachments: makeAttachments(attachments),
|
||||
}))
|
||||
}, deliveryMethod))
|
||||
|
||||
m, err := p.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mm := m.(Model)
|
||||
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
|
||||
},
|
||||
|
@ -101,6 +157,7 @@ var (
|
|||
CommitSHA string
|
||||
)
|
||||
|
||||
// CompletionCmd is the cobra command for generating completion scripts.
|
||||
var CompletionCmd = &cobra.Command{
|
||||
Use: "completion [bash|zsh|fish|powershell]",
|
||||
Short: "Generate completion script",
|
||||
|
@ -123,6 +180,7 @@ var CompletionCmd = &cobra.Command{
|
|||
},
|
||||
}
|
||||
|
||||
// ManCmd is the cobra command for the manual.
|
||||
var ManCmd = &cobra.Command{
|
||||
Use: "man",
|
||||
Short: "Generate man page",
|
||||
|
@ -144,19 +202,36 @@ var ManCmd = &cobra.Command{
|
|||
func init() {
|
||||
rootCmd.AddCommand(CompletionCmd, ManCmd)
|
||||
|
||||
rootCmd.Flags().StringSliceVar(&to, "bcc", []string{}, "BCC recipients")
|
||||
rootCmd.Flags().StringSliceVar(&to, "cc", []string{}, "CC recipients")
|
||||
rootCmd.Flags().StringSliceVar(&bcc, "bcc", []string{}, "BCC recipients")
|
||||
rootCmd.Flags().StringSliceVar(&cc, "cc", []string{}, "CC recipients")
|
||||
rootCmd.Flags().StringSliceVarP(&attachments, "attach", "a", []string{}, "Email's attachments")
|
||||
rootCmd.Flags().StringSliceVarP(&to, "to", "t", []string{}, "Recipients")
|
||||
rootCmd.Flags().StringVarP(&body, "body", "b", "", "Email's contents")
|
||||
envFrom := os.Getenv(POP_FROM)
|
||||
rootCmd.Flags().StringVarP(&from, "from", "f", envFrom, "Email's sender "+commentStyle.Render("($"+POP_FROM+")"))
|
||||
envFrom := os.Getenv(PopFrom)
|
||||
rootCmd.Flags().StringVarP(&from, "from", "f", envFrom, "Email's sender"+commentStyle.Render("($"+PopFrom+")"))
|
||||
rootCmd.Flags().StringVarP(&subject, "subject", "s", "", "Email's subject")
|
||||
rootCmd.Flags().BoolVarP(&preview, "preview", "p", false, "Whether to preview the email before sending")
|
||||
envUnsafe := os.Getenv(UNSAFE_HTML) == "true"
|
||||
rootCmd.Flags().BoolVar(&preview, "preview", false, "Whether to preview the email before sending")
|
||||
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)")
|
||||
envSignature := os.Getenv("POP_SIGNATURE")
|
||||
rootCmd.Flags().StringVarP(&signature, "signature", "x", envSignature, "Signature to display at the end of the email. "+commentStyle.Render("($"+POP_SIGNATURE+")"))
|
||||
envSignature := os.Getenv(PopSignature)
|
||||
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
|
||||
|
||||
|
|
40
model.go
40
model.go
|
@ -13,10 +13,11 @@ import (
|
|||
"github.com/charmbracelet/bubbles/textarea"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/x/exp/ordered"
|
||||
"github.com/resendlabs/resend-go"
|
||||
"golang.org/x/exp/constraints"
|
||||
)
|
||||
|
||||
// State is the current state of the application.
|
||||
type State int
|
||||
|
||||
const (
|
||||
|
@ -30,10 +31,26 @@ const (
|
|||
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 {
|
||||
// state represents the current state of the application.
|
||||
state State
|
||||
|
||||
// DeliveryMethod is whether we are using DeliveryMethod or Resend.
|
||||
DeliveryMethod DeliveryMethod
|
||||
|
||||
// From represents the sender's email address.
|
||||
From textinput.Model
|
||||
// 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.
|
||||
Attachments list.Model
|
||||
|
||||
Cc textinput.Model
|
||||
Bcc textinput.Model
|
||||
|
||||
// filepicker is used to pick file attachments.
|
||||
filepicker filepicker.Model
|
||||
loadingSpinner spinner.Model
|
||||
|
@ -58,7 +78,8 @@ type Model struct {
|
|||
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.Prompt = "From "
|
||||
from.Placeholder = "me@example.com"
|
||||
|
@ -76,7 +97,7 @@ func NewModel(defaults resend.SendEmailRequest) Model {
|
|||
to.PlaceholderStyle = placeholderStyle
|
||||
to.TextStyle = textStyle
|
||||
to.Placeholder = "you@example.com"
|
||||
to.SetValue(strings.Join(defaults.To, TO_SEPARATOR))
|
||||
to.SetValue(strings.Join(defaults.To, ToSeparator))
|
||||
|
||||
subject := textinput.New()
|
||||
subject.Prompt = "Subject "
|
||||
|
@ -155,6 +176,7 @@ func NewModel(defaults resend.SendEmailRequest) Model {
|
|||
help: help.New(),
|
||||
keymap: DefaultKeybinds(),
|
||||
loadingSpinner: loadingSpinner,
|
||||
DeliveryMethod: deliveryMethod,
|
||||
}
|
||||
|
||||
m.focusActiveInput()
|
||||
|
@ -162,6 +184,7 @@ func NewModel(defaults resend.SendEmailRequest) Model {
|
|||
return m
|
||||
}
|
||||
|
||||
// Init initializes the model.
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
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) {
|
||||
switch msg := msg.(type) {
|
||||
case sendEmailSuccessMsg:
|
||||
|
@ -243,7 +267,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
return m, m.filepicker.Init()
|
||||
case key.Matches(msg, m.keymap.Unattach):
|
||||
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):
|
||||
m.quitting = true
|
||||
m.abort = true
|
||||
|
@ -329,6 +353,7 @@ func (m *Model) focusActiveInput() {
|
|||
}
|
||||
}
|
||||
|
||||
// View displays the application.
|
||||
func (m Model) View() string {
|
||||
if m.quitting {
|
||||
return ""
|
||||
|
@ -371,10 +396,3 @@ func (m Model) View() 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")
|
||||
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)
|
||||
sendButtonInactiveStyle = lipgloss.NewStyle().Background(darkGrayColor).Foreground(lightGrayColor).Padding(0, 2)
|
||||
|
|
Loading…
Reference in a new issue