Add :attach command for compose
Allow users to add attachments to emails in the Compose view. Syntax is :attach <path>, where path is a valid file. Attachments will show up in the pre-send review screen.
This commit is contained in:
parent
fe7230bb9a
commit
7899d15d60
|
@ -0,0 +1,56 @@
|
||||||
|
package compose
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.sr.ht/~sircmpwn/aerc/widgets"
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
"github.com/mitchellh/go-homedir"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Attach struct{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
register(Attach{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_ Attach) Aliases() []string {
|
||||||
|
return []string{"attach"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_ Attach) Complete(aerc *widgets.Aerc, args []string) []string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_ Attach) Execute(aerc *widgets.Aerc, args []string) error {
|
||||||
|
if len(args) != 2 {
|
||||||
|
return fmt.Errorf("Usage: :attach <path>")
|
||||||
|
}
|
||||||
|
|
||||||
|
path := args[1]
|
||||||
|
|
||||||
|
path, err := homedir.Expand(path)
|
||||||
|
if err != nil {
|
||||||
|
aerc.PushError(" " + err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pathinfo, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
aerc.PushError(" " + err.Error())
|
||||||
|
return err
|
||||||
|
} else if pathinfo.IsDir() {
|
||||||
|
aerc.PushError("Attachment must be a file, not a directory")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
composer, _ := aerc.SelectedTab().(*widgets.Composer)
|
||||||
|
composer.AddAttachment(path)
|
||||||
|
|
||||||
|
aerc.PushStatus(fmt.Sprintf("Attached %s", pathinfo.Name()), 10*time.Second).
|
||||||
|
Color(tcell.ColorDefault, tcell.ColorGreen)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -158,6 +158,11 @@ message list, the message in the message viewer, etc).
|
||||||
*close*
|
*close*
|
||||||
Closes the message viewer.
|
Closes the message viewer.
|
||||||
|
|
||||||
|
## MESSAGE COMPOSE COMMANDS
|
||||||
|
|
||||||
|
*attach* <path>
|
||||||
|
Attaches the file at the given path to the email.
|
||||||
|
|
||||||
## TERMINAL COMMANDS
|
## TERMINAL COMMANDS
|
||||||
|
|
||||||
*close*
|
*close*
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
package widgets
|
package widgets
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
gomail "net/mail"
|
gomail "net/mail"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/emersion/go-message"
|
"github.com/emersion/go-message"
|
||||||
|
@ -32,6 +36,7 @@ type Composer struct {
|
||||||
defaults map[string]string
|
defaults map[string]string
|
||||||
editor *Terminal
|
editor *Terminal
|
||||||
email *os.File
|
email *os.File
|
||||||
|
attachments []string
|
||||||
grid *ui.Grid
|
grid *ui.Grid
|
||||||
review *reviewMessage
|
review *reviewMessage
|
||||||
worker *types.Worker
|
worker *types.Worker
|
||||||
|
@ -211,7 +216,6 @@ func (c *Composer) PrepareHeader() (*mail.Header, []string, error) {
|
||||||
}
|
}
|
||||||
// Update headers
|
// Update headers
|
||||||
mhdr := (*message.Header)(&header.Header)
|
mhdr := (*message.Header)(&header.Header)
|
||||||
mhdr.SetContentType("text/plain", map[string]string{"charset": "UTF-8"})
|
|
||||||
mhdr.SetText("Message-Id", mail.GenerateMessageID())
|
mhdr.SetText("Message-Id", mail.GenerateMessageID())
|
||||||
if subject, _ := header.Subject(); subject == "" {
|
if subject, _ := header.Subject(); subject == "" {
|
||||||
header.SetSubject(c.headers.subject.input.String())
|
header.SetSubject(c.headers.subject.input.String())
|
||||||
|
@ -302,18 +306,117 @@ func (c *Composer) WriteMessage(header *mail.Header, writer io.Writer) error {
|
||||||
c.email.Seek(0, os.SEEK_SET)
|
c.email.Seek(0, os.SEEK_SET)
|
||||||
body = c.email
|
body = c.email
|
||||||
}
|
}
|
||||||
// TODO: attachments
|
|
||||||
|
if len(c.attachments) == 0 {
|
||||||
|
// don't create a multipart email if we only have text
|
||||||
|
header.SetContentType("text/plain", map[string]string{"charset": "UTF-8"})
|
||||||
w, err := mail.CreateSingleInlineWriter(writer, *header)
|
w, err := mail.CreateSingleInlineWriter(writer, *header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "CreateSingleInlineWriter")
|
return errors.Wrap(err, "CreateSingleInlineWriter")
|
||||||
}
|
}
|
||||||
defer w.Close()
|
defer w.Close()
|
||||||
|
|
||||||
|
return writeBody(body, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise create a multipart email,
|
||||||
|
// with a multipart/alternative part for the text
|
||||||
|
w, err := mail.CreateWriter(writer, *header)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "CreateWriter")
|
||||||
|
}
|
||||||
|
defer w.Close()
|
||||||
|
|
||||||
|
bh := mail.InlineHeader{}
|
||||||
|
bh.SetContentType("text/plain", map[string]string{"charset": "UTF-8"})
|
||||||
|
|
||||||
|
bi, err := w.CreateInline()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "CreateInline")
|
||||||
|
}
|
||||||
|
defer bi.Close()
|
||||||
|
|
||||||
|
bw, err := bi.CreatePart(bh)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "CreatePart")
|
||||||
|
}
|
||||||
|
defer bw.Close()
|
||||||
|
|
||||||
|
if err := writeBody(body, bw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, a := range c.attachments {
|
||||||
|
writeAttachment(a, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeBody(body io.Reader, w io.Writer) error {
|
||||||
if _, err := io.Copy(w, body); err != nil {
|
if _, err := io.Copy(w, body); err != nil {
|
||||||
return errors.Wrap(err, "io.Copy")
|
return errors.Wrap(err, "io.Copy")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// write the attachment specified by path to the message
|
||||||
|
func writeAttachment(path string, writer *mail.Writer) error {
|
||||||
|
filename := filepath.Base(path)
|
||||||
|
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "os.Open")
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
reader := bufio.NewReader(f)
|
||||||
|
|
||||||
|
// determine the MIME type
|
||||||
|
// http.DetectContentType only cares about the first 512 bytes
|
||||||
|
head, err := reader.Peek(512)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Peek")
|
||||||
|
}
|
||||||
|
|
||||||
|
mimeString := http.DetectContentType(head)
|
||||||
|
// mimeString can contain type and params (like text encoding),
|
||||||
|
// so we need to break them apart before passing them to the headers
|
||||||
|
mimeType, params, err := mime.ParseMediaType(mimeString)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "ParseMediaType")
|
||||||
|
}
|
||||||
|
params["name"] = filename
|
||||||
|
|
||||||
|
// set header fields
|
||||||
|
ah := mail.AttachmentHeader{}
|
||||||
|
ah.SetContentType(mimeType, params)
|
||||||
|
// setting the filename auto sets the content disposition
|
||||||
|
ah.SetFilename(filename)
|
||||||
|
|
||||||
|
aw, err := writer.CreateAttachment(ah)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "CreateAttachment")
|
||||||
|
}
|
||||||
|
defer aw.Close()
|
||||||
|
|
||||||
|
if _, err := reader.WriteTo(aw); err != nil {
|
||||||
|
return errors.Wrap(err, "reader.WriteTo")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Composer) AddAttachment(path string) {
|
||||||
|
c.attachments = append(c.attachments, path)
|
||||||
|
if c.review != nil {
|
||||||
|
c.grid.RemoveChild(c.review)
|
||||||
|
c.review = newReviewMessage(c, nil)
|
||||||
|
c.grid.AddChild(c.review).At(1, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Composer) termClosed(err error) {
|
func (c *Composer) termClosed(err error) {
|
||||||
c.grid.RemoveChild(c.editor)
|
c.grid.RemoveChild(c.editor)
|
||||||
c.review = newReviewMessage(c, err)
|
c.review = newReviewMessage(c, err)
|
||||||
|
@ -412,13 +515,17 @@ type reviewMessage struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newReviewMessage(composer *Composer, err error) *reviewMessage {
|
func newReviewMessage(composer *Composer, err error) *reviewMessage {
|
||||||
grid := ui.NewGrid().Rows([]ui.GridSpec{
|
spec := []ui.GridSpec{{ui.SIZE_EXACT, 2}, {ui.SIZE_EXACT, 1}}
|
||||||
{ui.SIZE_EXACT, 2},
|
for range composer.attachments {
|
||||||
{ui.SIZE_EXACT, 1},
|
spec = append(spec, ui.GridSpec{ui.SIZE_EXACT, 1})
|
||||||
{ui.SIZE_WEIGHT, 1},
|
}
|
||||||
}).Columns([]ui.GridSpec{
|
// make the last element fill remaining space
|
||||||
|
spec = append(spec, ui.GridSpec{ui.SIZE_WEIGHT, 1})
|
||||||
|
|
||||||
|
grid := ui.NewGrid().Rows(spec).Columns([]ui.GridSpec{
|
||||||
{ui.SIZE_WEIGHT, 1},
|
{ui.SIZE_WEIGHT, 1},
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
grid.AddChild(ui.NewText(err.Error()).
|
grid.AddChild(ui.NewText(err.Error()).
|
||||||
Color(tcell.ColorRed, tcell.ColorDefault))
|
Color(tcell.ColorRed, tcell.ColorDefault))
|
||||||
|
@ -429,8 +536,13 @@ func newReviewMessage(composer *Composer, err error) *reviewMessage {
|
||||||
"Send this email? [y]es/[n]o/[e]dit")).At(0, 0)
|
"Send this email? [y]es/[n]o/[e]dit")).At(0, 0)
|
||||||
grid.AddChild(ui.NewText("Attachments:").
|
grid.AddChild(ui.NewText("Attachments:").
|
||||||
Reverse(true)).At(1, 0)
|
Reverse(true)).At(1, 0)
|
||||||
// TODO: Attachments
|
if len(composer.attachments) == 0 {
|
||||||
grid.AddChild(ui.NewText("(none)")).At(2, 0)
|
grid.AddChild(ui.NewText("(none)")).At(2, 0)
|
||||||
|
} else {
|
||||||
|
for i, a := range composer.attachments {
|
||||||
|
grid.AddChild(ui.NewText(a)).At(i+2, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &reviewMessage{
|
return &reviewMessage{
|
||||||
|
|
Loading…
Reference in New Issue