composer.go

  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				"Discard draft?",
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}