aerc

Working clone of aerc-mail.org
git clone git://git.danielmoch.com/aerc.git
Log | Files | Refs | README | LICENSE

compose.go (10496B)


      1 package widgets
      2 
      3 import (
      4 	"io"
      5 	"io/ioutil"
      6 	gomail "net/mail"
      7 	"os"
      8 	"os/exec"
      9 	"time"
     10 
     11 	"github.com/emersion/go-message"
     12 	"github.com/emersion/go-message/mail"
     13 	"github.com/gdamore/tcell"
     14 	"github.com/mattn/go-runewidth"
     15 	"github.com/pkg/errors"
     16 
     17 	"git.sr.ht/~sircmpwn/aerc/config"
     18 	"git.sr.ht/~sircmpwn/aerc/lib"
     19 	"git.sr.ht/~sircmpwn/aerc/lib/ui"
     20 	"git.sr.ht/~sircmpwn/aerc/worker/types"
     21 )
     22 
     23 type Composer struct {
     24 	headers struct {
     25 		from    *headerEditor
     26 		subject *headerEditor
     27 		to      *headerEditor
     28 	}
     29 
     30 	acct   *config.AccountConfig
     31 	config *config.AercConfig
     32 
     33 	defaults map[string]string
     34 	editor   *Terminal
     35 	email    *os.File
     36 	grid     *ui.Grid
     37 	review   *reviewMessage
     38 	worker   *types.Worker
     39 
     40 	focusable []ui.DrawableInteractive
     41 	focused   int
     42 }
     43 
     44 // TODO: Let caller configure headers, initial body (for replies), etc
     45 func NewComposer(conf *config.AercConfig,
     46 	acct *config.AccountConfig, worker *types.Worker) *Composer {
     47 
     48 	grid := ui.NewGrid().Rows([]ui.GridSpec{
     49 		{ui.SIZE_EXACT, 3},
     50 		{ui.SIZE_WEIGHT, 1},
     51 	}).Columns([]ui.GridSpec{
     52 		{ui.SIZE_WEIGHT, 1},
     53 	})
     54 
     55 	// TODO: let user specify extra headers to edit by default
     56 	headers := ui.NewGrid().Rows([]ui.GridSpec{
     57 		{ui.SIZE_EXACT, 1}, // To/From
     58 		{ui.SIZE_EXACT, 1}, // Subject
     59 		{ui.SIZE_EXACT, 1}, // [spacer]
     60 	}).Columns([]ui.GridSpec{
     61 		{ui.SIZE_WEIGHT, 1},
     62 		{ui.SIZE_WEIGHT, 1},
     63 	})
     64 
     65 	to := newHeaderEditor("To", "")
     66 	from := newHeaderEditor("From", acct.From)
     67 	subject := newHeaderEditor("Subject", "")
     68 	headers.AddChild(to).At(0, 0)
     69 	headers.AddChild(from).At(0, 1)
     70 	headers.AddChild(subject).At(1, 0).Span(1, 2)
     71 	headers.AddChild(ui.NewFill(' ')).At(2, 0).Span(1, 2)
     72 
     73 	email, err := ioutil.TempFile("", "aerc-compose-*.eml")
     74 	if err != nil {
     75 		// TODO: handle this better
     76 		return nil
     77 	}
     78 
     79 	grid.AddChild(headers).At(0, 0)
     80 
     81 	c := &Composer{
     82 		acct:   acct,
     83 		config: conf,
     84 		email:  email,
     85 		grid:   grid,
     86 		worker: worker,
     87 		// You have to backtab to get to "From", since you usually don't edit it
     88 		focused:   1,
     89 		focusable: []ui.DrawableInteractive{from, to, subject},
     90 	}
     91 	c.headers.to = to
     92 	c.headers.from = from
     93 	c.headers.subject = subject
     94 	c.ShowTerminal()
     95 
     96 	return c
     97 }
     98 
     99 // Sets additional headers to be added to the outgoing email (e.g. In-Reply-To)
    100 func (c *Composer) Defaults(defaults map[string]string) *Composer {
    101 	c.defaults = defaults
    102 	if to, ok := defaults["To"]; ok {
    103 		c.headers.to.input.Set(to)
    104 		delete(defaults, "To")
    105 	}
    106 	if from, ok := defaults["From"]; ok {
    107 		c.headers.from.input.Set(from)
    108 		delete(defaults, "From")
    109 	}
    110 	if subject, ok := defaults["Subject"]; ok {
    111 		c.headers.subject.input.Set(subject)
    112 		delete(defaults, "Subject")
    113 	}
    114 	return c
    115 }
    116 
    117 // Note: this does not reload the editor. You must call this before the first
    118 // Draw() call.
    119 func (c *Composer) SetContents(reader io.Reader) *Composer {
    120 	c.email.Seek(0, os.SEEK_SET)
    121 	io.Copy(c.email, reader)
    122 	c.email.Sync()
    123 	c.email.Seek(0, os.SEEK_SET)
    124 	return c
    125 }
    126 
    127 func (c *Composer) FocusTerminal() *Composer {
    128 	if c.editor == nil {
    129 		return c
    130 	}
    131 	c.focusable[c.focused].Focus(false)
    132 	c.focused = 3
    133 	c.focusable[c.focused].Focus(true)
    134 	return c
    135 }
    136 
    137 func (c *Composer) OnSubjectChange(fn func(subject string)) {
    138 	c.headers.subject.OnChange(func() {
    139 		fn(c.headers.subject.input.String())
    140 	})
    141 }
    142 
    143 func (c *Composer) Draw(ctx *ui.Context) {
    144 	c.grid.Draw(ctx)
    145 }
    146 
    147 func (c *Composer) Invalidate() {
    148 	c.grid.Invalidate()
    149 }
    150 
    151 func (c *Composer) OnInvalidate(fn func(d ui.Drawable)) {
    152 	c.grid.OnInvalidate(func(_ ui.Drawable) {
    153 		fn(c)
    154 	})
    155 }
    156 
    157 func (c *Composer) Close() {
    158 	if c.email != nil {
    159 		path := c.email.Name()
    160 		c.email.Close()
    161 		os.Remove(path)
    162 		c.email = nil
    163 	}
    164 	if c.editor != nil {
    165 		c.editor.Destroy()
    166 		c.editor = nil
    167 	}
    168 }
    169 
    170 func (c *Composer) Bindings() string {
    171 	if c.editor == nil {
    172 		return "compose::review"
    173 	} else if c.editor == c.focusable[c.focused] {
    174 		return "compose::editor"
    175 	} else {
    176 		return "compose"
    177 	}
    178 }
    179 
    180 func (c *Composer) Event(event tcell.Event) bool {
    181 	return c.focusable[c.focused].Event(event)
    182 }
    183 
    184 func (c *Composer) Focus(focus bool) {
    185 	c.focusable[c.focused].Focus(focus)
    186 }
    187 
    188 func (c *Composer) Config() *config.AccountConfig {
    189 	return c.acct
    190 }
    191 
    192 func (c *Composer) Worker() *types.Worker {
    193 	return c.worker
    194 }
    195 
    196 func (c *Composer) PrepareHeader() (*mail.Header, []string, error) {
    197 	// Extract headers from the email, if present
    198 	c.email.Seek(0, os.SEEK_SET)
    199 	var (
    200 		rcpts  []string
    201 		header mail.Header
    202 	)
    203 	reader, err := mail.CreateReader(c.email)
    204 	if err == nil {
    205 		header = reader.Header
    206 		defer reader.Close()
    207 	} else {
    208 		c.email.Seek(0, os.SEEK_SET)
    209 	}
    210 	// Update headers
    211 	mhdr := (*message.Header)(&header.Header)
    212 	mhdr.SetContentType("text/plain", map[string]string{"charset": "UTF-8"})
    213 	mhdr.SetText("Message-Id", lib.GenerateMessageId())
    214 	if subject, _ := header.Subject(); subject == "" {
    215 		header.SetSubject(c.headers.subject.input.String())
    216 	}
    217 	if date, err := header.Date(); err != nil || date == (time.Time{}) {
    218 		header.SetDate(time.Now())
    219 	}
    220 	if from, _ := mhdr.Text("From"); from == "" {
    221 		mhdr.SetText("From", c.headers.from.input.String())
    222 	}
    223 	if to := c.headers.to.input.String(); to != "" {
    224 		// Dammit Simon, this branch is 3x as long as it ought to be because
    225 		// your types aren't compatible enough with each other
    226 		to_rcpts, err := gomail.ParseAddressList(to)
    227 		if err != nil {
    228 			return nil, nil, errors.Wrapf(err, "ParseAddressList(%s)", to)
    229 		}
    230 		ed_rcpts, err := header.AddressList("To")
    231 		if err != nil {
    232 			return nil, nil, errors.Wrap(err, "AddressList(To)")
    233 		}
    234 		for _, addr := range to_rcpts {
    235 			ed_rcpts = append(ed_rcpts, (*mail.Address)(addr))
    236 		}
    237 		header.SetAddressList("To", ed_rcpts)
    238 		for _, addr := range ed_rcpts {
    239 			rcpts = append(rcpts, addr.Address)
    240 		}
    241 	}
    242 	if cc, _ := mhdr.Text("Cc"); cc != "" {
    243 		cc_rcpts, err := gomail.ParseAddressList(cc)
    244 		if err != nil {
    245 			return nil, nil, errors.Wrapf(err, "ParseAddressList(%s)", cc)
    246 		}
    247 		// TODO: Update when the user inputs Cc's through the UI
    248 		for _, addr := range cc_rcpts {
    249 			rcpts = append(rcpts, addr.Address)
    250 		}
    251 	}
    252 	if bcc, _ := mhdr.Text("Bcc"); bcc != "" {
    253 		bcc_rcpts, err := gomail.ParseAddressList(bcc)
    254 		if err != nil {
    255 			return nil, nil, errors.Wrapf(err, "ParseAddressList(%s)", bcc)
    256 		}
    257 		// TODO: Update when the user inputs Bcc's through the UI
    258 		for _, addr := range bcc_rcpts {
    259 			rcpts = append(rcpts, addr.Address)
    260 		}
    261 	}
    262 	// Merge in additional headers
    263 	txthdr := mhdr.Header
    264 	for key, value := range c.defaults {
    265 		if !txthdr.Has(key) && value != "" {
    266 			mhdr.SetText(key, value)
    267 		}
    268 	}
    269 	return &header, rcpts, nil
    270 }
    271 
    272 func (c *Composer) WriteMessage(header *mail.Header, writer io.Writer) error {
    273 	c.email.Seek(0, os.SEEK_SET)
    274 	var body io.Reader
    275 	reader, err := mail.CreateReader(c.email)
    276 	if err == nil {
    277 		// TODO: Do we want to let users write a full blown multipart email
    278 		// into the editor? If so this needs to change
    279 		part, err := reader.NextPart()
    280 		if err != nil {
    281 			return errors.Wrap(err, "reader.NextPart")
    282 		}
    283 		body = part.Body
    284 		defer reader.Close()
    285 	} else {
    286 		c.email.Seek(0, os.SEEK_SET)
    287 		body = c.email
    288 	}
    289 	// TODO: attachments
    290 	w, err := mail.CreateSingleInlineWriter(writer, *header)
    291 	if err != nil {
    292 		return errors.Wrap(err, "CreateSingleInlineWriter")
    293 	}
    294 	defer w.Close()
    295 	if _, err := io.Copy(w, body); err != nil {
    296 		return errors.Wrap(err, "io.Copy")
    297 	}
    298 	return nil
    299 }
    300 
    301 func (c *Composer) termClosed(err error) {
    302 	c.grid.RemoveChild(c.editor)
    303 	c.review = newReviewMessage(c, err)
    304 	c.grid.AddChild(c.review).At(1, 0)
    305 	c.editor.Destroy()
    306 	c.editor = nil
    307 	c.focusable = c.focusable[:len(c.focusable)-1]
    308 	if c.focused >= len(c.focusable) {
    309 		c.focused = len(c.focusable) - 1
    310 	}
    311 }
    312 
    313 func (c *Composer) ShowTerminal() {
    314 	if c.editor != nil {
    315 		return
    316 	}
    317 	if c.review != nil {
    318 		c.grid.RemoveChild(c.review)
    319 	}
    320 	editorName := c.config.Compose.Editor
    321 	if editorName == "" {
    322 		editorName = os.Getenv("EDITOR")
    323 	}
    324 	if editorName == "" {
    325 		editorName = "vi"
    326 	}
    327 	editor := exec.Command(editorName, c.email.Name())
    328 	c.editor, _ = NewTerminal(editor) // TODO: handle error
    329 	c.editor.OnClose = c.termClosed
    330 	c.grid.AddChild(c.editor).At(1, 0)
    331 	c.focusable = append(c.focusable, c.editor)
    332 }
    333 
    334 func (c *Composer) PrevField() {
    335 	c.focusable[c.focused].Focus(false)
    336 	c.focused--
    337 	if c.focused == -1 {
    338 		c.focused = len(c.focusable) - 1
    339 	}
    340 	c.focusable[c.focused].Focus(true)
    341 }
    342 
    343 func (c *Composer) NextField() {
    344 	c.focusable[c.focused].Focus(false)
    345 	c.focused = (c.focused + 1) % len(c.focusable)
    346 	c.focusable[c.focused].Focus(true)
    347 }
    348 
    349 type headerEditor struct {
    350 	name  string
    351 	input *ui.TextInput
    352 }
    353 
    354 func newHeaderEditor(name string, value string) *headerEditor {
    355 	return &headerEditor{
    356 		input: ui.NewTextInput(value),
    357 		name:  name,
    358 	}
    359 }
    360 
    361 func (he *headerEditor) Draw(ctx *ui.Context) {
    362 	name := he.name + " "
    363 	size := runewidth.StringWidth(name)
    364 	ctx.Fill(0, 0, size, ctx.Height(), ' ', tcell.StyleDefault)
    365 	ctx.Printf(0, 0, tcell.StyleDefault.Bold(true), "%s", name)
    366 	he.input.Draw(ctx.Subcontext(size, 0, ctx.Width()-size, 1))
    367 }
    368 
    369 func (he *headerEditor) Invalidate() {
    370 	he.input.Invalidate()
    371 }
    372 
    373 func (he *headerEditor) OnInvalidate(fn func(ui.Drawable)) {
    374 	he.input.OnInvalidate(func(_ ui.Drawable) {
    375 		fn(he)
    376 	})
    377 }
    378 
    379 func (he *headerEditor) Focus(focused bool) {
    380 	he.input.Focus(focused)
    381 }
    382 
    383 func (he *headerEditor) Event(event tcell.Event) bool {
    384 	return he.input.Event(event)
    385 }
    386 
    387 func (he *headerEditor) OnChange(fn func()) {
    388 	he.input.OnChange(func(_ *ui.TextInput) {
    389 		fn()
    390 	})
    391 }
    392 
    393 type reviewMessage struct {
    394 	composer *Composer
    395 	grid     *ui.Grid
    396 }
    397 
    398 func newReviewMessage(composer *Composer, err error) *reviewMessage {
    399 	grid := ui.NewGrid().Rows([]ui.GridSpec{
    400 		{ui.SIZE_EXACT, 2},
    401 		{ui.SIZE_EXACT, 1},
    402 		{ui.SIZE_WEIGHT, 1},
    403 	}).Columns([]ui.GridSpec{
    404 		{ui.SIZE_WEIGHT, 1},
    405 	})
    406 	if err != nil {
    407 		grid.AddChild(ui.NewText(err.Error()).
    408 			Color(tcell.ColorRed, tcell.ColorDefault))
    409 		grid.AddChild(ui.NewText("Press [q] to close this tab.")).At(1, 0)
    410 	} else {
    411 		// TODO: source this from actual keybindings?
    412 		grid.AddChild(ui.NewText(
    413 			"Send this email? [y]es/[n]o/[e]dit")).At(0, 0)
    414 		grid.AddChild(ui.NewText("Attachments:").
    415 			Reverse(true)).At(1, 0)
    416 		// TODO: Attachments
    417 		grid.AddChild(ui.NewText("(none)")).At(2, 0)
    418 	}
    419 
    420 	return &reviewMessage{
    421 		composer: composer,
    422 		grid:     grid,
    423 	}
    424 }
    425 
    426 func (rm *reviewMessage) Invalidate() {
    427 	rm.grid.Invalidate()
    428 }
    429 
    430 func (rm *reviewMessage) OnInvalidate(fn func(ui.Drawable)) {
    431 	rm.grid.OnInvalidate(func(_ ui.Drawable) {
    432 		fn(rm)
    433 	})
    434 }
    435 
    436 func (rm *reviewMessage) Draw(ctx *ui.Context) {
    437 	rm.grid.Draw(ctx)
    438 }