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