diff --git a/email.go b/email.go new file mode 100644 index 0000000..024d4cd --- /dev/null +++ b/email.go @@ -0,0 +1,53 @@ +package main + +import ( + "os" + + tea "github.com/charmbracelet/bubbletea" + "github.com/resendlabs/resend-go" +) + +// sendEmailSuccessMsg is the tea.Msg handled by Bubble Tea when the email has +// been sent successfully. +type sendEmailSuccessMsg struct{} + +// sendEmailFailureMsg is the tea.Msg handled by Bubble Tea when the email has +// failed to send. +type sendEmailFailureMsg error + +// sendEmailCmd returns a tea.Cmd that sends the email. +func (m Model) sendEmailCmd() tea.Cmd { + return func() tea.Msg { + attachments := make([]string, len(m.Attachments.Items())) + for i, a := range m.Attachments.Items() { + at, ok := a.(attachment) + if !ok { + continue + } + attachments[i] = string(at) + } + err := sendEmail(m.From.Value(), m.To.Value(), m.Subject.Value(), m.Body.Value(), attachments) + if err != nil { + return sendEmailFailureMsg(err) + } + return sendEmailSuccessMsg{} + } +} + +func sendEmail(from, to, subject, body string, attachments []string) error { + client := resend.NewClient(os.Getenv(RESEND_API_KEY)) + + request := &resend.SendEmailRequest{ + From: from, + To: []string{to}, + Subject: subject, + Html: body, + } + + _, err := client.Emails.Send(request) + if err != nil { + return err + } + + return nil +} diff --git a/go.mod b/go.mod index 86744f4..6a35f40 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,9 @@ go 1.19 require ( github.com/charmbracelet/bubbles v0.16.1 - github.com/charmbracelet/bubbletea v0.24.3-0.20230609163353-b80eb8303bba + github.com/charmbracelet/bubbletea v0.24.3-0.20230614142509-c0cc6aa1fb4f github.com/charmbracelet/lipgloss v0.7.1 + github.com/resendlabs/resend-go v1.6.0 github.com/spf13/cobra v1.7.0 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 ) @@ -28,7 +29,7 @@ require ( github.com/sahilm/fuzzy v0.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.6.0 // indirect + golang.org/x/sys v0.8.0 // indirect golang.org/x/term v0.6.0 // indirect golang.org/x/text v0.3.8 // indirect ) diff --git a/go.sum b/go.sum index 4755189..090169d 100644 --- a/go.sum +++ b/go.sum @@ -4,13 +4,14 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= -github.com/charmbracelet/bubbletea v0.24.3-0.20230609163353-b80eb8303bba h1:hWjlqzPiG3dWN5KZUAKKnqz/R8BlYqX1Aw+nk7riQss= -github.com/charmbracelet/bubbletea v0.24.3-0.20230609163353-b80eb8303bba/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= +github.com/charmbracelet/bubbletea v0.24.3-0.20230614142509-c0cc6aa1fb4f h1:piPKA2I5VF6qDNPoDxw33/1wr7C5AZV/nAziJ5P4pM8= +github.com/charmbracelet/bubbletea v0.24.3-0.20230614142509-c0cc6aa1fb4f/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= 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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -33,6 +34,9 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/resendlabs/resend-go v1.6.0 h1:QnXJv71HVKHS+IDv9anK5XpYCegHH5MLK1W3q4cAvjk= +github.com/resendlabs/resend-go v1.6.0/go.mod h1:yip1STH7Bqfm4fD0So5HgyNbt5taG5Cplc4xXxETyLI= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -43,16 +47,19 @@ github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 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= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index a6fa6f5..cd21f06 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,15 @@ package main import ( + "fmt" "os" tea "github.com/charmbracelet/bubbletea" "github.com/spf13/cobra" ) +const RESEND_API_KEY = "RESEND_API_KEY" + var rootCmd = &cobra.Command{ Use: "email", Short: "email is a command line interface for sending emails.", @@ -22,6 +25,13 @@ var rootCmd = &cobra.Command{ } func main() { + key := os.Getenv(RESEND_API_KEY) + if key == "" { + fmt.Printf("\n %s %s %s\n\n", errorHeaderStyle.String(), inlineCodeStyle.Render(RESEND_API_KEY), "environment variable is required.") + fmt.Printf(" %s %s\n\n", commentStyle.Render("You can grab one at"), linkStyle.Render("https://resend.com")) + os.Exit(1) + } + err := rootCmd.Execute() if err != nil { os.Exit(1) diff --git a/model.go b/model.go index c9752d9..3df21f2 100644 --- a/model.go +++ b/model.go @@ -51,6 +51,7 @@ type Model struct { help help.Model keymap KeyMap quitting bool + err error } func NewModel() Model { @@ -106,7 +107,7 @@ func NewModel() Model { picker := filepicker.New() picker.CurrentDirectory, _ = os.UserHomeDir() - loadingSpinner := spinner.NewModel() + loadingSpinner := spinner.New() loadingSpinner.Spinner = spinner.Dot return Model{ @@ -132,13 +133,12 @@ func (m Model) Init() tea.Cmd { func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.From.Width = msg.Width - 2 - m.To.Width = msg.Width - 2 - m.Subject.Width = msg.Width - 2 - m.Body.SetWidth(msg.Width - 2) - m.Attachments.SetWidth(msg.Width - 2) - + case sendEmailSuccessMsg: + m.quitting = true + return m, tea.Quit + case sendEmailFailureMsg: + m.state = editingFrom + m.err = msg case tea.KeyMsg: switch { case key.Matches(msg, m.keymap.NextInput): @@ -178,6 +178,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.state = sendingEmail return m, tea.Batch( m.loadingSpinner.Tick, + m.sendEmailCmd(), ) case key.Matches(msg, m.keymap.Attach): m.state = pickingFile @@ -295,6 +296,11 @@ func (m Model) View() string { s.WriteString("\n") s.WriteString(m.help.View(m.keymap)) + if m.err != nil { + s.WriteString("\n\n") + s.WriteString(errorStyle.Render(m.err.Error())) + } + return paddedStyle.Render(s.String()) } diff --git a/style.go b/style.go index c0edaff..619f3cc 100644 --- a/style.go +++ b/style.go @@ -19,4 +19,11 @@ var ( cursorStyle = lipgloss.NewStyle().Foreground(whiteColor) paddedStyle = lipgloss.NewStyle().Padding(1) + + 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")) + + inlineCodeStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5F87")).Background(lipgloss.Color("#3A3A3A")).Padding(0, 1) + linkStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#00AF87")).Underline(true) )