aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGalen Abell <galen@galenabell.com>2019-07-16 16:48:25 -0400
committerDrew DeVault <sir@cmpwn.com>2019-07-19 10:30:47 -0400
commit7899d15d607cd9122e731cd2d2a8e52ee523ce0c (patch)
treefa83a1ae3721f7201229d1dd5324863561ab8e2f
parentfe7230bb9acc5dc9cc8a982a35196dd6796b5360 (diff)
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.
-rw-r--r--commands/compose/attach.go56
-rw-r--r--doc/aerc.1.scd5
-rw-r--r--widgets/compose.go146
3 files changed, 190 insertions, 17 deletions
diff --git a/commands/compose/attach.go b/commands/compose/attach.go
new file mode 100644
index 0000000..43aa32d
--- /dev/null
+++ b/commands/compose/attach.go
@@ -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
+}
diff --git a/doc/aerc.1.scd b/doc/aerc.1.scd
index 750d2da..de82394 100644
--- a/doc/aerc.1.scd
+++ b/doc/aerc.1.scd
@@ -158,6 +158,11 @@ message list, the message in the message viewer, etc).
*close*
Closes the message viewer.
+## MESSAGE COMPOSE COMMANDS
+
+*attach* <path>
+ Attaches the file at the given path to the email.
+
## TERMINAL COMMANDS
*close*
diff --git a/widgets/compose.go b/widgets/compose.go
index a68bbe1..f1c8014 100644
--- a/widgets/compose.go
+++ b/widgets/compose.go
@@ -1,11 +1,15 @@
package widgets
import (
+ "bufio"
"io"
"io/ioutil"
+ "mime"
+ "net/http"
gomail "net/mail"
"os"
"os/exec"
+ "path/filepath"
"time"
"github.com/emersion/go-message"
@@ -29,12 +33,13 @@ type Composer struct {
acct *config.AccountConfig
config *config.AercConfig
- defaults map[string]string
- editor *Terminal
- email *os.File
- grid *ui.Grid
- review *reviewMessage
- worker *types.Worker
+ defaults map[string]string
+ editor *Terminal
+ email *os.File
+ attachments []string
+ grid *ui.Grid
+ review *reviewMessage
+ worker *types.Worker
focusable []ui.DrawableInteractive
focused int
@@ -211,7 +216,6 @@ func (c *Composer) PrepareHeader() (*mail.Header, []string, error) {
}
// Update headers
mhdr := (*message.Header)(&header.Header)
- mhdr.SetContentType("text/plain", map[string]string{"charset": "UTF-8"})
mhdr.SetText("Message-Id", mail.GenerateMessageID())
if subject, _ := header.Subject(); subject == "" {
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)
body = c.email
}
- // TODO: attachments
- w, err := mail.CreateSingleInlineWriter(writer, *header)
+
+ 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)
+ if err != nil {
+ return errors.Wrap(err, "CreateSingleInlineWriter")
+ }
+ 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, "CreateSingleInlineWriter")
+ 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 {
return errors.Wrap(err, "io.Copy")
}
+
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) {
c.grid.RemoveChild(c.editor)
c.review = newReviewMessage(c, err)
@@ -412,13 +515,17 @@ type reviewMessage struct {
}
func newReviewMessage(composer *Composer, err error) *reviewMessage {
- grid := ui.NewGrid().Rows([]ui.GridSpec{
- {ui.SIZE_EXACT, 2},
- {ui.SIZE_EXACT, 1},
- {ui.SIZE_WEIGHT, 1},
- }).Columns([]ui.GridSpec{
+ spec := []ui.GridSpec{{ui.SIZE_EXACT, 2}, {ui.SIZE_EXACT, 1}}
+ for range composer.attachments {
+ spec = append(spec, ui.GridSpec{ui.SIZE_EXACT, 1})
+ }
+ // 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},
})
+
if err != nil {
grid.AddChild(ui.NewText(err.Error()).
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)
grid.AddChild(ui.NewText("Attachments:").
Reverse(true)).At(1, 0)
- // TODO: Attachments
- grid.AddChild(ui.NewText("(none)")).At(2, 0)
+ if len(composer.attachments) == 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{