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