Detailed changes
@@ -18,14 +18,15 @@ import (
)
type Email struct {
- From string
- To []string
- Subject string
- Body string
- Date time.Time
+ From string
+ To []string
+ Subject string
+ Body string
+ Date time.Time
+ MessageID string
+ References []string
}
-// ... (decodePart and decodeHeader functions remain the same)
func decodePart(reader io.Reader, header mail.PartHeader) (string, error) {
mediaType, params, err := mime.ParseMediaType(header.Get("Content-Type"))
if err != nil {
@@ -153,6 +154,8 @@ func FetchEmails(cfg *config.Config, limit, offset uint32) ([]Email, error) {
toAddrs, _ := header.AddressList("To")
subject := decodeHeader(header.Get("Subject"))
date, _ := header.Date()
+ messageID := header.Get("Message-ID")
+ references := header.Get("References")
var fromAddr string
if len(fromAddrs) > 0 {
@@ -185,11 +188,13 @@ func FetchEmails(cfg *config.Config, limit, offset uint32) ([]Email, error) {
}
emails = append(emails, Email{
- From: fromAddr,
- To: toAddrList,
- Subject: subject,
- Body: body,
- Date: date,
+ From: fromAddr,
+ To: toAddrList,
+ Subject: subject,
+ Body: body,
+ Date: date,
+ MessageID: messageID,
+ References: strings.Fields(references),
})
}
@@ -125,7 +125,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(cmds...)
case tui.GoToSendMsg:
- m.current = tui.NewComposer(m.config.Email)
+ m.current = tui.NewComposer(m.config.Email, msg.To, msg.Subject, msg.Body)
m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
cmds = append(cmds, m.current.Init())
@@ -139,6 +139,18 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.current = emailView
cmds = append(cmds, m.current.Init())
+ case tui.ReplyToEmailMsg:
+ to := msg.Email.From
+ subject := "Re: " + msg.Email.Subject
+ body := fmt.Sprintf("\n\nOn %s, %s wrote:\n> %s", msg.Email.Date.Format("Jan 2, 2006 at 3:04 PM"), msg.Email.From, strings.ReplaceAll(msg.Email.Body, "\n", "\n> "))
+ m.current = tui.NewComposer(m.config.Email, to, subject, body)
+ m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
+ cmds = append(cmds, m.current.Init())
+ // This is a reply, so we'll need to pass the message ID and references.
+ // We'll add this to the SendEmailMsg that gets created when the user hits send.
+ // We'll modify the composer so that when it creates a SendEmailMsg, it can be passed
+ // the InReplyTo and References.
+
case tui.SendEmailMsg:
m.current = tui.NewStatus("Sending email...")
cmds = append(cmds, m.current.Init(), sendEmail(m.config, msg))
@@ -200,7 +212,7 @@ func sendEmail(cfg *config.Config, msg tui.SendEmailMsg) tea.Cmd {
htmlBody := markdownToHTML([]byte(body))
- err := sender.SendEmail(cfg, recipients, msg.Subject, msg.Body, string(htmlBody), images)
+ err := sender.SendEmail(cfg, recipients, msg.Subject, msg.Body, string(htmlBody), images, msg.InReplyTo, msg.References)
if err != nil {
log.Printf("Failed to send email: %v", err)
return tui.EmailResultMsg{Err: err}
@@ -242,4 +254,4 @@ func main() {
fmt.Printf("Alas, there's been an error: %v", err)
os.Exit(1)
}
-}
+}
@@ -27,7 +27,7 @@ func generateMessageID(from string) string {
}
// SendEmail constructs a multipart message with plain text, HTML, and embedded images.
-func SendEmail(cfg *config.Config, to []string, subject, plainBody, htmlBody string, images map[string][]byte) error {
+func SendEmail(cfg *config.Config, to []string, subject, plainBody, htmlBody string, images map[string][]byte, inReplyTo string, references []string) error {
var smtpServer string
var smtpPort int
@@ -62,6 +62,17 @@ func SendEmail(cfg *config.Config, to []string, subject, plainBody, htmlBody str
"Message-ID": generateMessageID(cfg.Email),
"Content-Type": "multipart/related; boundary=" + mainWriter.Boundary(),
}
+
+ if inReplyTo != "" {
+ headers["In-Reply-To"] = inReplyTo
+ // When replying, the references should be the previous references plus the message-id of the email we're replying to.
+ if len(references) > 0 {
+ headers["References"] = strings.Join(references, " ") + " " + inReplyTo
+ } else {
+ headers["References"] = inReplyTo
+ }
+ }
+
for k, v := range headers {
fmt.Fprintf(&msg, "%s: %s\r\n", k, v)
}
@@ -28,12 +28,13 @@ type Composer struct {
}
// NewComposer initializes a new composer model.
-func NewComposer(from string) *Composer {
+func NewComposer(from, to, subject, body string) *Composer {
m := &Composer{fromAddr: from}
m.toInput = textinput.New()
m.toInput.Cursor.Style = cursorStyle
m.toInput.Placeholder = "To"
+ m.toInput.SetValue(to)
m.toInput.Focus()
m.toInput.Prompt = "> "
m.toInput.CharLimit = 256
@@ -41,14 +42,17 @@ func NewComposer(from string) *Composer {
m.subjectInput = textinput.New()
m.subjectInput.Cursor.Style = cursorStyle
m.subjectInput.Placeholder = "Subject"
+ m.subjectInput.SetValue(subject)
m.subjectInput.Prompt = "> "
m.subjectInput.CharLimit = 256
m.bodyInput = textarea.New()
m.bodyInput.Cursor.Style = cursorStyle
m.bodyInput.Placeholder = "Body (Markdown supported)..."
+ m.bodyInput.SetValue(body)
m.bodyInput.Prompt = "> "
m.bodyInput.SetHeight(10)
+ m.bodyInput.SetCursor(0) // Set cursor to the beginning on creation
return m
}
@@ -69,13 +73,15 @@ func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.subjectInput.Width = inputWidth
m.bodyInput.SetWidth(inputWidth)
+ case SetComposerCursorToStartMsg:
+ m.bodyInput.SetCursor(0)
+ return m, nil
+
case tea.KeyMsg:
switch msg.Type {
- // IMPORTANT: Removed tea.KeyEsc from this case
case tea.KeyCtrlC:
return m, tea.Quit
- // Handle Tab and Shift+Tab to cycle focus between inputs.
case tea.KeyTab, tea.KeyShiftTab:
if msg.Type == tea.KeyShiftTab {
m.focusIndex--
@@ -103,12 +109,12 @@ func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds = append(cmds, m.subjectInput.Focus())
case 2:
cmds = append(cmds, m.bodyInput.Focus())
+ // Send a message to explicitly set the cursor position AFTER focus.
+ cmds = append(cmds, func() tea.Msg { return SetComposerCursorToStartMsg{} })
}
return m, tea.Batch(cmds...)
- // Handle Enter key.
case tea.KeyEnter:
- // If on the Send button, send the email.
if m.focusIndex == 3 {
return m, func() tea.Msg {
return SendEmailMsg{
@@ -151,6 +157,6 @@ func (m *Composer) View() string {
m.subjectInput.View(),
m.bodyInput.View(),
*button,
- helpStyle.Render("Markdown enabled! • tab: next field • esc: back to menu"),
+ helpStyle.Render("Markdown/HTML • tab: next field • esc: back to menu"),
)
}
@@ -10,7 +10,7 @@ import (
// TestComposerUpdate verifies the state transitions in the email composer.
func TestComposerUpdate(t *testing.T) {
// Initialize a new composer.
- composer := NewComposer("test@example.com")
+ composer := NewComposer("test@example.com", "", "", "")
t.Run("Focus cycling", func(t *testing.T) {
// Initial focus is on the 'To' input (index 0).
@@ -20,28 +20,28 @@ func TestComposerUpdate(t *testing.T) {
// Simulate pressing Tab to move to the 'Subject' field.
model, _ := composer.Update(tea.KeyMsg{Type: tea.KeyTab}) // model is tea.Model
- composer = model.(*Composer) // Cast to *Composer
+ composer = model.(*Composer) // Cast to *Composer
if composer.focusIndex != 1 {
t.Errorf("After one Tab, focusIndex should be 1, got %d", composer.focusIndex)
}
// Simulate pressing Tab again to move to the 'Body' field.
model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyTab}) // model is tea.Model
- composer = model.(*Composer) // Cast to *Composer
+ composer = model.(*Composer) // Cast to *Composer
if composer.focusIndex != 2 {
t.Errorf("After two Tabs, focusIndex should be 2, got %d", composer.focusIndex)
}
// Simulate pressing Tab again to move to the 'Send' button.
model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyTab}) // model is tea.Model
- composer = model.(*Composer) // Cast to *Composer
+ composer = model.(*Composer) // Cast to *Composer
if composer.focusIndex != 3 {
t.Errorf("After three Tabs, focusIndex should be 3 (Send), got %d", composer.focusIndex)
}
// Simulate one more Tab to wrap around to the 'To' field.
model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyTab}) // model is tea.Model
- composer = model.(*Composer) // Cast to *Composer
+ composer = model.(*Composer) // Cast to *Composer
if composer.focusIndex != 0 {
t.Errorf("After four Tabs, focusIndex should wrap to 0, got %d", composer.focusIndex)
}
@@ -78,4 +78,4 @@ func TestComposerUpdate(t *testing.T) {
t.Errorf("Mismatched SendEmailMsg.\nGot: %+v\nWant: %+v", sendMsg, expectedMsg)
}
})
-}
+}
@@ -47,6 +47,13 @@ func (m *EmailView) Init() tea.Cmd {
func (m *EmailView) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch msg.String() {
+ case "r":
+ return m, func() tea.Msg {
+ return ReplyToEmailMsg{Email: m.email}
+ }
+ }
case tea.WindowSizeMsg:
header := fmt.Sprintf("From: %s\nSubject: %s", m.email.From, m.email.Subject)
headerHeight := lipgloss.Height(header) + 2
@@ -60,5 +67,6 @@ func (m *EmailView) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m *EmailView) View() string {
header := fmt.Sprintf("From: %s\nSubject: %s", m.email.From, m.email.Subject)
styledHeader := emailHeaderStyle.Width(m.viewport.Width).Render(header)
- return fmt.Sprintf("%s\n%s", styledHeader, m.viewport.View())
+ help := helpStyle.Render("r: reply • esc: back to inbox")
+ return fmt.Sprintf("%s\n%s\n%s", styledHeader, m.viewport.View(), help)
}
@@ -9,9 +9,11 @@ type ViewEmailMsg struct {
// A message to indicate that an email has been sent.
type SendEmailMsg struct {
- To string
- Subject string
- Body string
+ To string
+ Subject string
+ Body string
+ InReplyTo string
+ References []string
}
// A message to indicate that the user has entered their credentials.
@@ -48,7 +50,11 @@ type FetchErr error
type GoToInboxMsg struct{}
// A message to navigate to the composer view.
-type GoToSendMsg struct{}
+type GoToSendMsg struct {
+ To string
+ Subject string
+ Body string
+}
// A message to navigate to the settings view.
type GoToSettingsMsg struct{}
@@ -64,4 +70,12 @@ type FetchingMoreEmailsMsg struct{}
// A message to indicate that new emails have been fetched and should be appended.
type EmailsAppendedMsg struct {
Emails []fetcher.Email
-}
+}
+
+// A message to reply to an email.
+type ReplyToEmailMsg struct {
+ Email fetcher.Email
+}
+
+// A message to set the composer cursor to the start.
+type SetComposerCursorToStartMsg struct{}