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