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