diff --git a/email.go b/email.go index 024d4cd..aa74a28 100644 --- a/email.go +++ b/email.go @@ -1,12 +1,17 @@ package main import ( + "bytes" "os" + "strings" tea "github.com/charmbracelet/bubbletea" "github.com/resendlabs/resend-go" + "github.com/yuin/goldmark" ) +const TO_SEPARATOR = "," + // sendEmailSuccessMsg is the tea.Msg handled by Bubble Tea when the email has // been sent successfully. type sendEmailSuccessMsg struct{} @@ -26,7 +31,7 @@ func (m Model) sendEmailCmd() tea.Cmd { } attachments[i] = string(at) } - err := sendEmail(m.From.Value(), m.To.Value(), m.Subject.Value(), m.Body.Value(), attachments) + err := sendEmail(strings.Split(m.To.Value(), TO_SEPARATOR), m.From.Value(), m.Subject.Value(), m.Body.Value(), attachments) if err != nil { return sendEmailFailureMsg(err) } @@ -34,17 +39,24 @@ func (m Model) sendEmailCmd() tea.Cmd { } } -func sendEmail(from, to, subject, body string, attachments []string) error { +func sendEmail(to []string, from, subject, body string, attachments []string) error { client := resend.NewClient(os.Getenv(RESEND_API_KEY)) + var html, text = bytes.NewBufferString(""), bytes.NewBufferString("") + err := goldmark.Convert([]byte(body), html) + if err != nil { + text.WriteString(body) + } + request := &resend.SendEmailRequest{ From: from, - To: []string{to}, + To: to, Subject: subject, - Html: body, + Html: html.String(), + Text: text.String(), } - _, err := client.Emails.Send(request) + _, err = client.Emails.Send(request) if err != nil { return err } diff --git a/go.mod b/go.mod index 6a35f40..71fe70f 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/charmbracelet/lipgloss v0.7.1 github.com/resendlabs/resend-go v1.6.0 github.com/spf13/cobra v1.7.0 + github.com/yuin/goldmark v1.5.4 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 ) diff --git a/go.sum b/go.sum index 090169d..697c57a 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,8 @@ 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/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= +github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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= diff --git a/main.go b/main.go index cd21f06..f9e2c9a 100644 --- a/main.go +++ b/main.go @@ -2,20 +2,52 @@ package main import ( "fmt" + "io" "os" tea "github.com/charmbracelet/bubbletea" + "github.com/resendlabs/resend-go" "github.com/spf13/cobra" ) const RESEND_API_KEY = "RESEND_API_KEY" +const RESEND_FROM = "RESEND_FROM" + +var ( + from string + to []string + subject string + body string + attachments []string +) var rootCmd = &cobra.Command{ Use: "email", Short: "email is a command line interface for sending emails.", Long: `email is a command line interface for sending emails.`, RunE: func(cmd *cobra.Command, args []string) error { - p := tea.NewProgram(NewModel()) + if hasStdin() { + b, err := io.ReadAll(os.Stdin) + if err != nil { + return err + } + body = string(b) + } + + if len(to) > 0 && from != "" && subject != "" && body != "" { + err := sendEmail(to, from, subject, body, attachments) + if err != nil { + return err + } + return nil + } + + p := tea.NewProgram(NewModel(resend.SendEmailRequest{ + From: from, + To: to, + Subject: subject, + Text: body, + })) _, err := p.Run() if err != nil { return err @@ -24,6 +56,22 @@ var rootCmd = &cobra.Command{ }, } +// hasStdin returns whether there is data in stdin. +func hasStdin() bool { + stat, err := os.Stdin.Stat() + return err == nil && (stat.Mode()&os.ModeCharDevice) == 0 +} + +func init() { + rootCmd.Flags().StringSliceVar(&to, "bcc", []string{}, "Blind carbon copy recipients") + rootCmd.Flags().StringSliceVar(&to, "cc", []string{}, "Carbon copy recipients") + rootCmd.Flags().StringSliceVarP(&attachments, "attach", "a", []string{}, "Email's attachments") + rootCmd.Flags().StringSliceVarP(&to, "to", "t", []string{}, "Recipient emails") + rootCmd.Flags().StringVarP(&body, "body", "b", "", "Email's body (markdown)") + rootCmd.Flags().StringVarP(&from, "from", "f", os.Getenv(RESEND_FROM), "Email's sender ($RESEND_FROM)") + rootCmd.Flags().StringVarP(&subject, "subject", "s", "", "Email's subject") +} + func main() { key := os.Getenv(RESEND_API_KEY) if key == "" { diff --git a/model.go b/model.go index 0f711e5..a99acc1 100644 --- a/model.go +++ b/model.go @@ -12,6 +12,7 @@ import ( "github.com/charmbracelet/bubbles/textarea" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" + "github.com/resendlabs/resend-go" "golang.org/x/exp/constraints" ) @@ -54,7 +55,7 @@ type Model struct { err error } -func NewModel() Model { +func NewModel(defaults resend.SendEmailRequest) Model { from := textinput.New() from.Prompt = "From " from.Placeholder = "me@example.com" @@ -63,6 +64,7 @@ func NewModel() Model { from.TextStyle = activeTextStyle from.Cursor.Style = cursorStyle from.PlaceholderStyle = placeholderStyle + from.SetValue(defaults.From) from.Focus() to := textinput.New() @@ -70,14 +72,18 @@ func NewModel() Model { to.PromptStyle = labelStyle.Copy() to.Cursor.Style = cursorStyle to.PlaceholderStyle = placeholderStyle + to.TextStyle = textStyle to.Placeholder = "you@example.com" + to.SetValue(strings.Join(defaults.To, TO_SEPARATOR)) subject := textinput.New() subject.Prompt = "Subject " subject.PromptStyle = labelStyle.Copy() subject.Cursor.Style = cursorStyle subject.PlaceholderStyle = placeholderStyle + subject.TextStyle = textStyle subject.Placeholder = "Hello!" + subject.SetValue(defaults.Subject) body := textarea.New() body.Placeholder = "# Email" @@ -91,6 +97,8 @@ func NewModel() Model { body.BlurredStyle.Text = textStyle body.BlurredStyle.Placeholder = placeholderStyle body.Cursor.Style = cursorStyle + body.CharLimit = 4000 + body.SetValue(defaults.Text) body.Blur() attachments := list.New([]list.Item{}, attachmentDelegate{}, 0, 3)