Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SMTP Support #17

Merged
merged 3 commits into from
Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 [email protected]
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 [email protected]
export POP_SIGNATURE="Sent with [Pop](https://github.com/charmbracelet/pop)!"
```
Expand Down
2 changes: 1 addition & 1 deletion attachments.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
92 changes: 87 additions & 5 deletions email.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -30,16 +35,91 @@ 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)
}
return sendEmailSuccessMsg{}
}
}

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

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.
Expand All @@ -62,10 +142,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)
Expand Down
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@ 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 (
github.com/atotto/clipboard v0.1.4 // indirect
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
Expand All @@ -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
Expand Down
12 changes: 10 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
21 changes: 20 additions & 1 deletion go.work.sum
Original file line number Diff line number Diff line change
@@ -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=
3 changes: 3 additions & 0 deletions keymap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand All @@ -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},
Expand Down
Loading
Loading