1package tui
2
3import (
4 "fmt"
5 "path/filepath"
6 "strings"
7
8 "charm.land/bubbles/v2/textarea"
9 "charm.land/bubbles/v2/textinput"
10 tea "charm.land/bubbletea/v2"
11 "charm.land/lipgloss/v2"
12 "github.com/floatpane/matcha/config"
13 "github.com/google/uuid"
14)
15
16var (
17 suggestionStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("245"))
18 selectedSuggestionStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).Bold(true)
19 suggestionBoxStyle = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color("245")).Padding(0, 1)
20)
21
22// Styles for the UI
23var (
24 focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42"))
25 blurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("245"))
26 noStyle = lipgloss.NewStyle()
27 helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("245"))
28 focusedButton = focusedStyle.Copy().Render("[ Send ]")
29 blurredButton = blurredStyle.Copy().Render("[ Send ]")
30 emailRecipientStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).Bold(true)
31 attachmentStyle = lipgloss.NewStyle().PaddingLeft(4).Foreground(lipgloss.Color("245"))
32 fromSelectorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42"))
33 smimeToggleStyle = lipgloss.NewStyle().PaddingLeft(4).Foreground(lipgloss.Color("245"))
34)
35
36const (
37 focusFrom = iota
38 focusTo
39 focusCc
40 focusBcc
41 focusSubject
42 focusBody
43 focusSignature
44 focusAttachment
45 focusEncryptSMIME
46 focusSend
47)
48
49// Composer model holds the state of the email composition UI.
50type Composer struct {
51 focusIndex int
52 toInput textinput.Model
53 ccInput textinput.Model
54 bccInput textinput.Model
55 subjectInput textinput.Model
56 bodyInput textarea.Model
57 signatureInput textarea.Model
58 attachmentPaths []string
59 encryptSMIME bool
60 width int
61 height int
62 confirmingExit bool
63 hideTips bool
64
65 // Multi-account support
66 accounts []config.Account
67 selectedAccountIdx int
68 showAccountPicker bool
69
70 // Contact suggestions
71 suggestions []config.Contact
72 selectedSuggestion int
73 showSuggestions bool
74 lastToValue string
75
76 // Draft persistence
77 draftID string
78
79 // Reply context
80 inReplyTo string
81 references []string
82
83 // Hidden quoted text (appended to body when sending, but not shown in editor)
84 quotedText string
85
86 // Plugin status text shown in the help bar
87 pluginStatus string
88}
89
90// NewComposer initializes a new composer model.
91func NewComposer(from, to, subject, body string, hideTips bool) *Composer {
92 m := &Composer{
93 draftID: uuid.New().String(),
94 hideTips: hideTips,
95 }
96
97 tiStyles := ThemedTextInputStyles()
98 taStyles := ThemedTextAreaStyles()
99
100 m.toInput = textinput.New()
101 m.toInput.Placeholder = "To"
102 m.toInput.SetValue(to)
103 m.toInput.Prompt = "> "
104 m.toInput.CharLimit = 256
105 m.toInput.SetStyles(tiStyles)
106
107 m.ccInput = textinput.New()
108 m.ccInput.Placeholder = "Cc"
109 m.ccInput.Prompt = "> "
110 m.ccInput.CharLimit = 256
111 m.ccInput.SetStyles(tiStyles)
112
113 m.bccInput = textinput.New()
114 m.bccInput.Placeholder = "Bcc"
115 m.bccInput.Prompt = "> "
116 m.bccInput.CharLimit = 256
117 m.bccInput.SetStyles(tiStyles)
118
119 m.subjectInput = textinput.New()
120 m.subjectInput.Placeholder = "Subject"
121 m.subjectInput.SetValue(subject)
122 m.subjectInput.Prompt = "> "
123 m.subjectInput.CharLimit = 256
124 m.subjectInput.SetStyles(tiStyles)
125
126 m.bodyInput = textarea.New()
127 m.bodyInput.Placeholder = "Body (Markdown supported)..."
128 m.bodyInput.SetValue(body)
129 m.bodyInput.Prompt = "> "
130 m.bodyInput.SetHeight(10)
131 m.bodyInput.SetStyles(taStyles)
132
133 m.signatureInput = textarea.New()
134 m.signatureInput.Placeholder = "Signature (optional)..."
135 m.signatureInput.Prompt = "> "
136 m.signatureInput.SetHeight(3)
137 m.signatureInput.SetStyles(taStyles)
138 // Load default signature
139 if sig, err := config.LoadSignature(); err == nil && sig != "" {
140 m.signatureInput.SetValue(sig)
141 }
142
143 // Start focus on To field (From is selectable but not a text input)
144 m.focusIndex = focusTo
145 m.toInput.Focus()
146
147 return m
148}
149
150// NewComposerWithAccounts initializes a composer with multiple account support.
151func NewComposerWithAccounts(accounts []config.Account, selectedAccountID string, to, subject, body string, hideTips bool) *Composer {
152 m := NewComposer("", to, subject, body, hideTips)
153 m.accounts = accounts
154
155 // Find the selected account index
156 for i, acc := range accounts {
157 if acc.ID == selectedAccountID {
158 m.selectedAccountIdx = i
159 break
160 }
161 }
162
163 return m
164}
165
166// ResetConfirmation ensures a restored draft isnt stuck in the exit prompt.
167func (m *Composer) ResetConfirmation() {
168 m.confirmingExit = false
169}
170
171func (m *Composer) Init() tea.Cmd {
172 return textinput.Blink
173}
174
175func (m *Composer) getFromAddress() string {
176 if len(m.accounts) > 0 && m.selectedAccountIdx < len(m.accounts) {
177 acc := m.accounts[m.selectedAccountIdx]
178 if acc.Name != "" {
179 return fmt.Sprintf("%s <%s>", acc.Name, acc.FetchEmail)
180 }
181 return acc.FetchEmail
182 }
183 return ""
184}
185
186func (m *Composer) getSelectedAccount() *config.Account {
187 if len(m.accounts) > 0 && m.selectedAccountIdx < len(m.accounts) {
188 return &m.accounts[m.selectedAccountIdx]
189 }
190 return nil
191}
192
193func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
194 var cmds []tea.Cmd
195 var cmd tea.Cmd
196
197 switch msg := msg.(type) {
198 case tea.WindowSizeMsg:
199 m.width = msg.Width
200 m.height = msg.Height
201 inputWidth := msg.Width - 6
202 m.toInput.SetWidth(inputWidth)
203 m.ccInput.SetWidth(inputWidth)
204 m.bccInput.SetWidth(inputWidth)
205 m.subjectInput.SetWidth(inputWidth)
206 m.bodyInput.SetWidth(inputWidth)
207 m.signatureInput.SetWidth(inputWidth)
208 if msg.Height > 0 {
209 // Fixed rows: title, from, to, cc, bcc, subject, sig label,
210 // attachment, smime, button, blank, tip, help = 13
211 const fixedRows = 13
212 available := msg.Height - fixedRows
213 if available < 6 {
214 available = 6
215 }
216 bodyHeight := (available * 55) / 100
217 sigHeight := (available * 15) / 100
218 if bodyHeight < 3 {
219 bodyHeight = 3
220 }
221 if sigHeight < 2 {
222 sigHeight = 2
223 }
224 m.bodyInput.SetHeight(bodyHeight)
225 m.signatureInput.SetHeight(sigHeight)
226 }
227
228 case FileSelectedMsg:
229 // Avoid duplicates
230 for _, p := range m.attachmentPaths {
231 if p == msg.Path {
232 return m, nil
233 }
234 }
235 m.attachmentPaths = append(m.attachmentPaths, msg.Path)
236 return m, nil
237
238 case tea.KeyPressMsg:
239 // Handle contact suggestions mode
240 if m.showSuggestions && len(m.suggestions) > 0 {
241 switch msg.String() {
242 case "up", "ctrl+p":
243 if m.selectedSuggestion > 0 {
244 m.selectedSuggestion--
245 }
246 return m, nil
247 case "down", "ctrl+n":
248 if m.selectedSuggestion < len(m.suggestions)-1 {
249 m.selectedSuggestion++
250 }
251 return m, nil
252 case "tab", "enter":
253 // Select the suggestion
254 selected := m.suggestions[m.selectedSuggestion]
255
256 var newEmail string
257 if strings.Contains(selected.Email, ",") {
258 // It's a mailing list: insert just the addresses to maintain valid email formatting
259 newEmail = selected.Email
260 } else if selected.Name != "" && selected.Name != selected.Email {
261 newEmail = fmt.Sprintf("%s <%s>", selected.Name, selected.Email)
262 } else {
263 newEmail = selected.Email
264 }
265
266 parts := strings.Split(m.toInput.Value(), ",")
267 if len(parts) > 0 {
268 if len(parts) == 1 {
269 parts[0] = newEmail
270 } else {
271 parts[len(parts)-1] = " " + newEmail
272 }
273 } else {
274 parts = []string{newEmail}
275 }
276
277 finalValue := strings.Join(parts, ",")
278 if !strings.HasSuffix(finalValue, ", ") {
279 finalValue += ", "
280 }
281
282 m.toInput.SetValue(finalValue)
283 m.toInput.SetCursor(len(finalValue))
284 m.lastToValue = m.toInput.Value()
285 m.showSuggestions = false
286 m.suggestions = nil
287 return m, nil
288 case "esc":
289 m.showSuggestions = false
290 m.suggestions = nil
291 return m, nil
292 }
293 // For shift+tab, close suggestions and let it fall through to normal handling
294 if msg.String() == "shift+tab" {
295 m.showSuggestions = false
296 m.suggestions = nil
297 }
298 }
299
300 // Handle account picker mode
301 if m.showAccountPicker {
302 switch msg.String() {
303 case "up", "k":
304 if m.selectedAccountIdx > 0 {
305 m.selectedAccountIdx--
306 }
307 case "down", "j":
308 if m.selectedAccountIdx < len(m.accounts)-1 {
309 m.selectedAccountIdx++
310 }
311 case "enter":
312 m.showAccountPicker = false
313 case "esc":
314 m.showAccountPicker = false
315 }
316 return m, nil
317 }
318
319 if m.confirmingExit {
320 switch msg.String() {
321 case "y", "Y":
322 return m, func() tea.Msg { return DiscardDraftMsg{ComposerState: m} }
323 case "n", "N", "esc":
324 m.confirmingExit = false
325 return m, nil
326 default:
327 return m, nil
328 }
329 }
330
331 switch msg.String() {
332 case "ctrl+c":
333 return m, tea.Quit
334 case "ctrl+e":
335 return m, func() tea.Msg { return OpenEditorMsg{} }
336 case "esc":
337 m.confirmingExit = true
338 return m, nil
339
340 case "tab", "shift+tab":
341 if msg.String() == "shift+tab" {
342 m.focusIndex--
343 } else {
344 m.focusIndex++
345 }
346
347 maxFocus := focusSend
348 minFocus := focusFrom
349 // Skip From field if only one account (nothing to switch)
350 if len(m.accounts) <= 1 {
351 minFocus = focusTo
352 }
353
354 if m.focusIndex > maxFocus {
355 m.focusIndex = minFocus
356 } else if m.focusIndex < minFocus {
357 m.focusIndex = maxFocus
358 }
359
360 m.toInput.Blur()
361 m.ccInput.Blur()
362 m.bccInput.Blur()
363 m.subjectInput.Blur()
364 m.bodyInput.Blur()
365 m.signatureInput.Blur()
366
367 switch m.focusIndex {
368 case focusTo:
369 cmds = append(cmds, m.toInput.Focus())
370 case focusCc:
371 cmds = append(cmds, m.ccInput.Focus())
372 case focusBcc:
373 cmds = append(cmds, m.bccInput.Focus())
374 case focusSubject:
375 cmds = append(cmds, m.subjectInput.Focus())
376 case focusBody:
377 cmds = append(cmds, m.bodyInput.Focus())
378 case focusSignature:
379 cmds = append(cmds, m.signatureInput.Focus())
380 }
381 return m, tea.Batch(cmds...)
382
383 case "backspace", "delete", "d":
384 if m.focusIndex == focusAttachment && len(m.attachmentPaths) > 0 {
385 m.attachmentPaths = m.attachmentPaths[:len(m.attachmentPaths)-1]
386 return m, nil
387 }
388
389 case "enter", " ":
390 switch m.focusIndex {
391 case focusFrom:
392 if len(m.accounts) > 1 && msg.String() == "enter" {
393 m.showAccountPicker = true
394 }
395 return m, nil
396 case focusAttachment:
397 if msg.String() == "enter" {
398 return m, func() tea.Msg { return GoToFilePickerMsg{} }
399 }
400 case focusEncryptSMIME:
401 if msg.String() == "enter" || msg.String() == " " {
402 m.encryptSMIME = !m.encryptSMIME
403 }
404 return m, nil
405
406 case focusSend:
407 if msg.String() == "enter" {
408 acc := m.getSelectedAccount()
409 accountID := ""
410 if acc != nil {
411 accountID = acc.ID
412 }
413 return m, func() tea.Msg {
414 return SendEmailMsg{
415 To: m.toInput.Value(),
416 Cc: m.ccInput.Value(),
417 Bcc: m.bccInput.Value(),
418 Subject: m.subjectInput.Value(),
419 Body: m.bodyInput.Value(),
420 AttachmentPaths: m.attachmentPaths,
421 AccountID: accountID,
422 QuotedText: m.quotedText,
423 InReplyTo: m.inReplyTo,
424 References: m.references,
425 Signature: m.signatureInput.Value(),
426 SignSMIME: acc != nil && acc.SMIMESignByDefault,
427 EncryptSMIME: m.encryptSMIME,
428 SignPGP: acc != nil && acc.PGPSignByDefault,
429 }
430 }
431 }
432 }
433 }
434 }
435
436 switch m.focusIndex {
437 case focusTo:
438 m.toInput, cmd = m.toInput.Update(msg)
439 cmds = append(cmds, cmd)
440
441 // Check if To field value changed and update suggestions
442 currentValue := m.toInput.Value()
443 if currentValue != m.lastToValue {
444 m.lastToValue = currentValue
445
446 // Extract the last comma-separated part for searching
447 parts := strings.Split(currentValue, ",")
448 lastPart := strings.TrimSpace(parts[len(parts)-1])
449
450 if len(lastPart) >= 2 {
451 m.suggestions = config.SearchContacts(lastPart)
452 m.showSuggestions = len(m.suggestions) > 0
453 m.selectedSuggestion = 0
454 } else {
455 m.showSuggestions = false
456 m.suggestions = nil
457 }
458 }
459 case focusCc:
460 m.ccInput, cmd = m.ccInput.Update(msg)
461 cmds = append(cmds, cmd)
462 case focusBcc:
463 m.bccInput, cmd = m.bccInput.Update(msg)
464 cmds = append(cmds, cmd)
465 case focusSubject:
466 m.subjectInput, cmd = m.subjectInput.Update(msg)
467 cmds = append(cmds, cmd)
468 case focusBody:
469 m.bodyInput, cmd = m.bodyInput.Update(msg)
470 cmds = append(cmds, cmd)
471 case focusSignature:
472 m.signatureInput, cmd = m.signatureInput.Update(msg)
473 cmds = append(cmds, cmd)
474 }
475
476 return m, tea.Batch(cmds...)
477}
478
479func (m *Composer) View() tea.View {
480 var composerView strings.Builder
481 var button string
482
483 if m.focusIndex == focusSend {
484 button = focusedButton
485 } else {
486 button = blurredButton
487 }
488
489 // From field with account selector
490 fromAddr := m.getFromAddress()
491 var fromField string
492 if len(m.accounts) > 1 {
493 if m.focusIndex == focusFrom {
494 fromField = focusedStyle.Render(fmt.Sprintf("> From: %s [Enter to switch]", fromAddr))
495 } else {
496 fromField = blurredStyle.Render(fmt.Sprintf(" From: %s [switchable]", fromAddr))
497 }
498 } else if fromAddr != "" {
499 fromField = " From: " + emailRecipientStyle.Render(fromAddr)
500 } else {
501 fromField = blurredStyle.Render(" From: (no account configured)")
502 }
503
504 var attachmentField string
505 if len(m.attachmentPaths) == 0 {
506 attachmentText := "None (Enter to add)"
507 if m.focusIndex == focusAttachment {
508 attachmentField = focusedStyle.Render(fmt.Sprintf("> Attachments: %s", attachmentText))
509 } else {
510 attachmentField = blurredStyle.Render(fmt.Sprintf(" Attachments: %s", attachmentText))
511 }
512 } else {
513 var names []string
514 for _, p := range m.attachmentPaths {
515 names = append(names, filepath.Base(p))
516 }
517 attachmentText := strings.Join(names, ", ")
518 if m.focusIndex == focusAttachment {
519 attachmentField = focusedStyle.Render(fmt.Sprintf("> Attachments (%d): %s", len(m.attachmentPaths), attachmentText))
520 } else {
521 attachmentField = blurredStyle.Render(fmt.Sprintf(" Attachments (%d): %s", len(m.attachmentPaths), attachmentText))
522 }
523 }
524
525 encToggle := "[ ]"
526 if m.encryptSMIME {
527 encToggle = "[x]"
528 }
529 encField := blurredStyle.Render(fmt.Sprintf(" Encrypt Email (S/MIME): %s", encToggle))
530 if m.focusIndex == focusEncryptSMIME {
531 encField = focusedStyle.Render(fmt.Sprintf("> Encrypt Email (S/MIME): %s", encToggle))
532 }
533
534 // Build To field with suggestions
535 toFieldView := m.toInput.View()
536 if m.showSuggestions && len(m.suggestions) > 0 {
537 var suggestionsBuilder strings.Builder
538 for i, s := range m.suggestions {
539 display := s.Email
540 if s.Name != "" && s.Name != s.Email {
541 display = fmt.Sprintf("%s <%s>", s.Name, s.Email)
542 }
543 if i == m.selectedSuggestion {
544 suggestionsBuilder.WriteString(selectedSuggestionStyle.Render("> "+display) + "\n")
545 } else {
546 suggestionsBuilder.WriteString(suggestionStyle.Render(" "+display) + "\n")
547 }
548 }
549 toFieldView = toFieldView + "\n" + suggestionBoxStyle.Render(strings.TrimSuffix(suggestionsBuilder.String(), "\n"))
550 }
551
552 // Signature field label
553 var signatureLabel string
554 if m.focusIndex == focusSignature {
555 signatureLabel = focusedStyle.Render("Signature:")
556 } else {
557 signatureLabel = blurredStyle.Render("Signature:")
558 }
559
560 tip := ""
561 switch m.focusIndex {
562 case focusFrom:
563 tip = "Select the account to send from."
564 case focusTo:
565 tip = "Enter recipient email addresses."
566 case focusCc:
567 tip = "Carbon copy recipients."
568 case focusBcc:
569 tip = "Blind carbon copy recipients."
570 case focusSubject:
571 tip = "The subject line of your email."
572 case focusBody:
573 tip = "The main content of your email. Markdown and HTML are supported."
574 case focusSignature:
575 tip = "Your email signature. This will be appended to the end of the email."
576 case focusAttachment:
577 tip = "Enter: add file • backspace/d: remove last attachment"
578 case focusEncryptSMIME:
579 tip = "Press Space or Enter to toggle S/MIME encryption on or off."
580 case focusSend:
581 tip = "Press Enter to send the email."
582 }
583
584 composerViewElements := []string{
585 "Compose New Email",
586 fromField,
587 toFieldView,
588 m.ccInput.View(),
589 m.bccInput.View(),
590 m.subjectInput.View(),
591 m.bodyInput.View(),
592 signatureLabel,
593 m.signatureInput.View(),
594 attachmentStyle.Render(attachmentField),
595 smimeToggleStyle.Render(encField),
596 button,
597 "",
598 }
599
600 if !m.hideTips && tip != "" {
601 composerViewElements = append(composerViewElements, TipStyle.Render("Tip: "+tip))
602 }
603
604 mainContent := lipgloss.JoinVertical(lipgloss.Left, composerViewElements...)
605 helpText := "Markdown/HTML • tab/shift+tab: navigate • ctrl+e: $EDITOR • esc: save draft & exit"
606 if m.pluginStatus != "" {
607 helpText += " • " + m.pluginStatus
608 }
609 helpView := helpStyle.Render(helpText)
610
611 if m.height > 0 {
612 currentHeight := lipgloss.Height(mainContent) + lipgloss.Height(helpView)
613 gap := m.height - currentHeight
614 if gap >= 0 {
615 mainContent += strings.Repeat("\n", gap+1)
616 } else {
617 mainContent += "\n"
618 }
619 } else {
620 mainContent += "\n\n"
621 }
622
623 composerView.WriteString(mainContent)
624 composerView.WriteString(helpView)
625
626 // Account picker overlay
627 if m.showAccountPicker {
628 var accountList strings.Builder
629 accountList.WriteString("Select Account:\n\n")
630 for i, acc := range m.accounts {
631 display := acc.FetchEmail
632 if acc.Name != "" {
633 display = fmt.Sprintf("%s (%s)", acc.Name, acc.FetchEmail)
634 }
635 if i == m.selectedAccountIdx {
636 accountList.WriteString(selectedItemStyle.Render(fmt.Sprintf("> %s", display)))
637 } else {
638 accountList.WriteString(itemStyle.Render(fmt.Sprintf(" %s", display)))
639 }
640 accountList.WriteString("\n")
641 }
642 accountList.WriteString("\n")
643 accountList.WriteString(HelpStyle.Render("↑/↓: navigate • enter: select • esc: cancel"))
644
645 dialog := DialogBoxStyle.Render(accountList.String())
646 return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog))
647 }
648
649 if m.confirmingExit {
650 dialog := DialogBoxStyle.Render(
651 lipgloss.JoinVertical(lipgloss.Center,
652 "Are you sure you want to exit? This draft will be saved",
653 HelpStyle.Render("\n(y/n)"),
654 ),
655 )
656 return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog))
657 }
658
659 return tea.NewView(composerView.String())
660}
661
662// SetAccounts sets the available accounts for sending.
663func (m *Composer) SetAccounts(accounts []config.Account) {
664 m.accounts = accounts
665 if m.selectedAccountIdx >= len(accounts) {
666 m.selectedAccountIdx = 0
667 }
668}
669
670// SetSelectedAccount sets the selected account by ID.
671func (m *Composer) SetSelectedAccount(accountID string) {
672 for i, acc := range m.accounts {
673 if acc.ID == accountID {
674 m.selectedAccountIdx = i
675 return
676 }
677 }
678}
679
680// GetSelectedAccountID returns the ID of the currently selected account.
681func (m *Composer) GetSelectedAccountID() string {
682 if len(m.accounts) > 0 && m.selectedAccountIdx < len(m.accounts) {
683 return m.accounts[m.selectedAccountIdx].ID
684 }
685 return ""
686}
687
688// GetDraftID returns the draft ID for this composer.
689func (m *Composer) GetDraftID() string {
690 return m.draftID
691}
692
693// SetDraftID sets the draft ID (for loading existing drafts).
694func (m *Composer) SetDraftID(id string) {
695 m.draftID = id
696}
697
698// GetTo returns the current To field value.
699func (m *Composer) GetTo() string {
700 return m.toInput.Value()
701}
702
703// SetTo updates the To field with new content.
704func (m *Composer) SetTo(to string) {
705 m.toInput.SetValue(to)
706}
707
708// GetCc returns the current Cc field value.
709func (m *Composer) GetCc() string {
710 return m.ccInput.Value()
711}
712
713// SetCc updates the Cc field with new content.
714func (m *Composer) SetCc(cc string) {
715 m.ccInput.SetValue(cc)
716}
717
718// GetBcc returns the current Bcc field value.
719func (m *Composer) GetBcc() string {
720 return m.bccInput.Value()
721}
722
723// SetBcc updates the Bcc field with new content.
724func (m *Composer) SetBcc(bcc string) {
725 m.bccInput.SetValue(bcc)
726}
727
728// GetSubject returns the current Subject field value.
729func (m *Composer) GetSubject() string {
730 return m.subjectInput.Value()
731}
732
733// SetSubject updates the Subject field with new content.
734func (m *Composer) SetSubject(subject string) {
735 m.subjectInput.SetValue(subject)
736}
737
738// GetBody returns the current Body field value.
739func (m *Composer) GetBody() string {
740 return m.bodyInput.Value()
741}
742
743// SetBody updates the Body field with new content.
744func (m *Composer) SetBody(body string) {
745 m.bodyInput.SetValue(body)
746}
747
748// GetAttachmentPaths returns the current attachment paths.
749func (m *Composer) GetAttachmentPaths() []string {
750 return m.attachmentPaths
751}
752
753// GetSignature returns the current signature value.
754func (m *Composer) GetSignature() string {
755 return m.signatureInput.Value()
756}
757
758// SetReplyContext sets the reply context for the draft.
759func (m *Composer) SetReplyContext(inReplyTo string, references []string) {
760 m.inReplyTo = inReplyTo
761 m.references = references
762}
763
764// SetQuotedText sets the hidden quoted text that will be appended when sending.
765func (m *Composer) SetQuotedText(text string) {
766 m.quotedText = text
767}
768
769// GetQuotedText returns the hidden quoted text.
770func (m *Composer) GetQuotedText() string {
771 return m.quotedText
772}
773
774// GetInReplyTo returns the In-Reply-To header value.
775func (m *Composer) GetInReplyTo() string {
776 return m.inReplyTo
777}
778
779// GetReferences returns the References header values.
780func (m *Composer) GetReferences() []string {
781 return m.references
782}
783
784// SetPluginStatus sets a persistent status string from plugins, shown in the help bar.
785func (m *Composer) SetPluginStatus(status string) {
786 m.pluginStatus = status
787}
788
789// ToDraft converts the composer state to a Draft for saving.
790func (m *Composer) ToDraft() config.Draft {
791 return config.Draft{
792 ID: m.draftID,
793 To: m.toInput.Value(),
794 Cc: m.ccInput.Value(),
795 Bcc: m.bccInput.Value(),
796 Subject: m.subjectInput.Value(),
797 Body: m.bodyInput.Value(),
798 AttachmentPaths: m.attachmentPaths,
799 AccountID: m.GetSelectedAccountID(),
800 InReplyTo: m.inReplyTo,
801 References: m.references,
802 QuotedText: m.quotedText,
803 }
804}
805
806// NewComposerFromDraft creates a composer from an existing draft.
807func NewComposerFromDraft(draft config.Draft, accounts []config.Account, hideTips bool) *Composer {
808 m := NewComposerWithAccounts(accounts, draft.AccountID, draft.To, draft.Subject, draft.Body, hideTips)
809 m.ccInput.SetValue(draft.Cc)
810 m.bccInput.SetValue(draft.Bcc)
811 m.draftID = draft.ID
812 m.attachmentPaths = draft.AttachmentPaths
813 m.inReplyTo = draft.InReplyTo
814 m.references = draft.References
815 m.quotedText = draft.QuotedText
816 return m
817}