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	// 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			case focusSend:
406				if msg.String() == "enter" {
407					acc := m.getSelectedAccount()
408					accountID := ""
409					if acc != nil {
410						accountID = acc.ID
411					}
412					return m, func() tea.Msg {
413						return SendEmailMsg{
414							To:              m.toInput.Value(),
415							Cc:              m.ccInput.Value(),
416							Bcc:             m.bccInput.Value(),
417							Subject:         m.subjectInput.Value(),
418							Body:            m.bodyInput.Value(),
419							AttachmentPaths: m.attachmentPaths,
420							AccountID:       accountID,
421							QuotedText:      m.quotedText,
422							InReplyTo:       m.inReplyTo,
423							References:      m.references,
424							Signature:       m.signatureInput.Value(),
425							SignSMIME:       acc != nil && acc.SMIMESignByDefault,
426							EncryptSMIME:    m.encryptSMIME,
427						}
428					}
429				}
430			}
431		}
432	}
433
434	switch m.focusIndex {
435	case focusTo:
436		m.toInput, cmd = m.toInput.Update(msg)
437		cmds = append(cmds, cmd)
438
439		// Check if To field value changed and update suggestions
440		currentValue := m.toInput.Value()
441		if currentValue != m.lastToValue {
442			m.lastToValue = currentValue
443
444			// Extract the last comma-separated part for searching
445			parts := strings.Split(currentValue, ",")
446			lastPart := strings.TrimSpace(parts[len(parts)-1])
447
448			if len(lastPart) >= 2 {
449				m.suggestions = config.SearchContacts(lastPart)
450				m.showSuggestions = len(m.suggestions) > 0
451				m.selectedSuggestion = 0
452			} else {
453				m.showSuggestions = false
454				m.suggestions = nil
455			}
456		}
457	case focusCc:
458		m.ccInput, cmd = m.ccInput.Update(msg)
459		cmds = append(cmds, cmd)
460	case focusBcc:
461		m.bccInput, cmd = m.bccInput.Update(msg)
462		cmds = append(cmds, cmd)
463	case focusSubject:
464		m.subjectInput, cmd = m.subjectInput.Update(msg)
465		cmds = append(cmds, cmd)
466	case focusBody:
467		m.bodyInput, cmd = m.bodyInput.Update(msg)
468		cmds = append(cmds, cmd)
469	case focusSignature:
470		m.signatureInput, cmd = m.signatureInput.Update(msg)
471		cmds = append(cmds, cmd)
472	}
473
474	return m, tea.Batch(cmds...)
475}
476
477func (m *Composer) View() tea.View {
478	var composerView strings.Builder
479	var button string
480
481	if m.focusIndex == focusSend {
482		button = focusedButton
483	} else {
484		button = blurredButton
485	}
486
487	// From field with account selector
488	fromAddr := m.getFromAddress()
489	var fromField string
490	if len(m.accounts) > 1 {
491		if m.focusIndex == focusFrom {
492			fromField = focusedStyle.Render(fmt.Sprintf("> From: %s [Enter to switch]", fromAddr))
493		} else {
494			fromField = blurredStyle.Render(fmt.Sprintf("  From: %s [switchable]", fromAddr))
495		}
496	} else if fromAddr != "" {
497		fromField = "  From: " + emailRecipientStyle.Render(fromAddr)
498	} else {
499		fromField = blurredStyle.Render("  From: (no account configured)")
500	}
501
502	var attachmentField string
503	if len(m.attachmentPaths) == 0 {
504		attachmentText := "None (Enter to add)"
505		if m.focusIndex == focusAttachment {
506			attachmentField = focusedStyle.Render(fmt.Sprintf("> Attachments: %s", attachmentText))
507		} else {
508			attachmentField = blurredStyle.Render(fmt.Sprintf("  Attachments: %s", attachmentText))
509		}
510	} else {
511		var names []string
512		for _, p := range m.attachmentPaths {
513			names = append(names, filepath.Base(p))
514		}
515		attachmentText := strings.Join(names, ", ")
516		if m.focusIndex == focusAttachment {
517			attachmentField = focusedStyle.Render(fmt.Sprintf("> Attachments (%d): %s", len(m.attachmentPaths), attachmentText))
518		} else {
519			attachmentField = blurredStyle.Render(fmt.Sprintf("  Attachments (%d): %s", len(m.attachmentPaths), attachmentText))
520		}
521	}
522
523	encToggle := "[ ]"
524	if m.encryptSMIME {
525		encToggle = "[x]"
526	}
527	encField := blurredStyle.Render(fmt.Sprintf("  Encrypt Email (S/MIME): %s", encToggle))
528	if m.focusIndex == focusEncryptSMIME {
529		encField = focusedStyle.Render(fmt.Sprintf("> Encrypt Email (S/MIME): %s", encToggle))
530	}
531
532	// Build To field with suggestions
533	toFieldView := m.toInput.View()
534	if m.showSuggestions && len(m.suggestions) > 0 {
535		var suggestionsBuilder strings.Builder
536		for i, s := range m.suggestions {
537			display := s.Email
538			if s.Name != "" && s.Name != s.Email {
539				display = fmt.Sprintf("%s <%s>", s.Name, s.Email)
540			}
541			if i == m.selectedSuggestion {
542				suggestionsBuilder.WriteString(selectedSuggestionStyle.Render("> "+display) + "\n")
543			} else {
544				suggestionsBuilder.WriteString(suggestionStyle.Render("  "+display) + "\n")
545			}
546		}
547		toFieldView = toFieldView + "\n" + suggestionBoxStyle.Render(strings.TrimSuffix(suggestionsBuilder.String(), "\n"))
548	}
549
550	// Signature field label
551	var signatureLabel string
552	if m.focusIndex == focusSignature {
553		signatureLabel = focusedStyle.Render("Signature:")
554	} else {
555		signatureLabel = blurredStyle.Render("Signature:")
556	}
557
558	tip := ""
559	switch m.focusIndex {
560	case focusFrom:
561		tip = "Select the account to send from."
562	case focusTo:
563		tip = "Enter recipient email addresses."
564	case focusCc:
565		tip = "Carbon copy recipients."
566	case focusBcc:
567		tip = "Blind carbon copy recipients."
568	case focusSubject:
569		tip = "The subject line of your email."
570	case focusBody:
571		tip = "The main content of your email. Markdown and HTML are supported."
572	case focusSignature:
573		tip = "Your email signature. This will be appended to the end of the email."
574	case focusAttachment:
575		tip = "Enter: add file • backspace/d: remove last attachment"
576	case focusEncryptSMIME:
577		tip = "Press Space or Enter to toggle S/MIME encryption on or off."
578	case focusSend:
579		tip = "Press Enter to send the email."
580	}
581
582	composerViewElements := []string{
583		"Compose New Email",
584		fromField,
585		toFieldView,
586		m.ccInput.View(),
587		m.bccInput.View(),
588		m.subjectInput.View(),
589		m.bodyInput.View(),
590		signatureLabel,
591		m.signatureInput.View(),
592		attachmentStyle.Render(attachmentField),
593		smimeToggleStyle.Render(encField),
594		button,
595		"",
596	}
597
598	if !m.hideTips && tip != "" {
599		composerViewElements = append(composerViewElements, TipStyle.Render("Tip: "+tip))
600	}
601
602	mainContent := lipgloss.JoinVertical(lipgloss.Left, composerViewElements...)
603	helpText := "Markdown/HTML • tab/shift+tab: navigate • ctrl+e: $EDITOR • esc: save draft & exit"
604	if m.pluginStatus != "" {
605		helpText += " • " + m.pluginStatus
606	}
607	helpView := helpStyle.Render(helpText)
608
609	if m.height > 0 {
610		currentHeight := lipgloss.Height(mainContent) + lipgloss.Height(helpView)
611		gap := m.height - currentHeight
612		if gap >= 0 {
613			mainContent += strings.Repeat("\n", gap+1)
614		} else {
615			mainContent += "\n"
616		}
617	} else {
618		mainContent += "\n\n"
619	}
620
621	composerView.WriteString(mainContent)
622	composerView.WriteString(helpView)
623
624	// Account picker overlay
625	if m.showAccountPicker {
626		var accountList strings.Builder
627		accountList.WriteString("Select Account:\n\n")
628		for i, acc := range m.accounts {
629			display := acc.FetchEmail
630			if acc.Name != "" {
631				display = fmt.Sprintf("%s (%s)", acc.Name, acc.FetchEmail)
632			}
633			if i == m.selectedAccountIdx {
634				accountList.WriteString(selectedItemStyle.Render(fmt.Sprintf("> %s", display)))
635			} else {
636				accountList.WriteString(itemStyle.Render(fmt.Sprintf("  %s", display)))
637			}
638			accountList.WriteString("\n")
639		}
640		accountList.WriteString("\n")
641		accountList.WriteString(HelpStyle.Render("↑/↓: navigate • enter: select • esc: cancel"))
642
643		dialog := DialogBoxStyle.Render(accountList.String())
644		return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog))
645	}
646
647	if m.confirmingExit {
648		dialog := DialogBoxStyle.Render(
649			lipgloss.JoinVertical(lipgloss.Center,
650				"Are you sure you want to exit? This draft will be saved",
651				HelpStyle.Render("\n(y/n)"),
652			),
653		)
654		return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog))
655	}
656
657	return tea.NewView(composerView.String())
658}
659
660// SetAccounts sets the available accounts for sending.
661func (m *Composer) SetAccounts(accounts []config.Account) {
662	m.accounts = accounts
663	if m.selectedAccountIdx >= len(accounts) {
664		m.selectedAccountIdx = 0
665	}
666}
667
668// SetSelectedAccount sets the selected account by ID.
669func (m *Composer) SetSelectedAccount(accountID string) {
670	for i, acc := range m.accounts {
671		if acc.ID == accountID {
672			m.selectedAccountIdx = i
673			return
674		}
675	}
676}
677
678// GetSelectedAccountID returns the ID of the currently selected account.
679func (m *Composer) GetSelectedAccountID() string {
680	if len(m.accounts) > 0 && m.selectedAccountIdx < len(m.accounts) {
681		return m.accounts[m.selectedAccountIdx].ID
682	}
683	return ""
684}
685
686// GetDraftID returns the draft ID for this composer.
687func (m *Composer) GetDraftID() string {
688	return m.draftID
689}
690
691// SetDraftID sets the draft ID (for loading existing drafts).
692func (m *Composer) SetDraftID(id string) {
693	m.draftID = id
694}
695
696// GetTo returns the current To field value.
697func (m *Composer) GetTo() string {
698	return m.toInput.Value()
699}
700
701// GetSubject returns the current Subject field value.
702func (m *Composer) GetSubject() string {
703	return m.subjectInput.Value()
704}
705
706// GetBody returns the current Body field value.
707func (m *Composer) GetBody() string {
708	return m.bodyInput.Value()
709}
710
711// SetBody updates the Body field with new content.
712func (m *Composer) SetBody(body string) {
713	m.bodyInput.SetValue(body)
714}
715
716// GetAttachmentPaths returns the current attachment paths.
717func (m *Composer) GetAttachmentPaths() []string {
718	return m.attachmentPaths
719}
720
721// GetSignature returns the current signature value.
722func (m *Composer) GetSignature() string {
723	return m.signatureInput.Value()
724}
725
726// SetReplyContext sets the reply context for the draft.
727func (m *Composer) SetReplyContext(inReplyTo string, references []string) {
728	m.inReplyTo = inReplyTo
729	m.references = references
730}
731
732// SetQuotedText sets the hidden quoted text that will be appended when sending.
733func (m *Composer) SetQuotedText(text string) {
734	m.quotedText = text
735}
736
737// GetQuotedText returns the hidden quoted text.
738func (m *Composer) GetQuotedText() string {
739	return m.quotedText
740}
741
742// GetInReplyTo returns the In-Reply-To header value.
743func (m *Composer) GetInReplyTo() string {
744	return m.inReplyTo
745}
746
747// GetReferences returns the References header values.
748func (m *Composer) GetReferences() []string {
749	return m.references
750}
751
752// SetPluginStatus sets a persistent status string from plugins, shown in the help bar.
753func (m *Composer) SetPluginStatus(status string) {
754	m.pluginStatus = status
755}
756
757// ToDraft converts the composer state to a Draft for saving.
758func (m *Composer) ToDraft() config.Draft {
759	return config.Draft{
760		ID:              m.draftID,
761		To:              m.toInput.Value(),
762		Cc:              m.ccInput.Value(),
763		Bcc:             m.bccInput.Value(),
764		Subject:         m.subjectInput.Value(),
765		Body:            m.bodyInput.Value(),
766		AttachmentPaths: m.attachmentPaths,
767		AccountID:       m.GetSelectedAccountID(),
768		InReplyTo:       m.inReplyTo,
769		References:      m.references,
770		QuotedText:      m.quotedText,
771	}
772}
773
774// NewComposerFromDraft creates a composer from an existing draft.
775func NewComposerFromDraft(draft config.Draft, accounts []config.Account, hideTips bool) *Composer {
776	m := NewComposerWithAccounts(accounts, draft.AccountID, draft.To, draft.Subject, draft.Body, hideTips)
777	m.ccInput.SetValue(draft.Cc)
778	m.bccInput.SetValue(draft.Bcc)
779	m.draftID = draft.ID
780	m.attachmentPaths = draft.AttachmentPaths
781	m.inReplyTo = draft.InReplyTo
782	m.references = draft.References
783	m.quotedText = draft.QuotedText
784	return m
785}