SMTP Support (#17)

This commit is contained in:
Maas Lalani 2023-07-31 10:32:02 -04:00 committed by GitHub
parent db68f79f1d
commit 7397bcb80e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 289 additions and 50 deletions

View file

@ -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)!"
```

View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -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=

View file

@ -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=

View file

@ -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
View file

@ -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

View file

@ -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
}

View file

@ -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)