From cbcabfafaab20eaffad642f20151e890161efddc Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Tue, 6 Dec 2022 20:08:49 +0100 Subject: compose: allow writing multipart/alternative messages Add a new :multipart command that can be executed on the composer review screen. This command takes a MIME type as argument which needs to match a setting in the new [multipart-converters] section of aerc.conf. A part can be removed by using the -d flag. The [multipart-converters] section has MIME types associated with commands. These commands are executed with sh -c every time the main email body is updated to generate each part content. The commands are expected to output valid UTF-8 text. If a command fails, an explicit error will be printed next to the part MIME type to allow users to debug their issue but the email may still be sent anyway with an empty alternative part. This is mostly intended for people who *really* need to send html messages for their boss or for corporate reasons. For now, it is a manual and explicit action to convert a message in such a way. Here is an example configuration: [multipart-converters] text/html = pandoc -f markdown -t html And the associated binding to append an HTML alternative to a message: [compose::review] H = :multipart text/html hh = :multipart -d text/html Link: https://lists.sr.ht/~rjarry/aerc-discuss/%3CCO5KH4W57XNB.2PZLR1CNFK22H%40mashenka%3E Co-authored-by: Eric McConville Signed-off-by: Robin Jarry Tested-by: Bence Ferdinandy Acked-by: Moritz Poldrack --- widgets/compose.go | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) (limited to 'widgets') diff --git a/widgets/compose.go b/widgets/compose.go index 30c5268d..37e09ccb 100644 --- a/widgets/compose.go +++ b/widgets/compose.go @@ -464,6 +464,11 @@ func (c *Composer) AppendPart(mimetype string, params map[string]string, body io if !strings.HasPrefix(mimetype, "text") { return fmt.Errorf("can only append text mimetypes") } + for _, part := range c.textParts { + if part.MimeType == mimetype { + return fmt.Errorf("%s part already exists", mimetype) + } + } newPart, err := lib.NewPart(mimetype, params, body) if err != nil { return err @@ -473,6 +478,21 @@ func (c *Composer) AppendPart(mimetype string, params map[string]string, body io return nil } +func (c *Composer) RemovePart(mimetype string) error { + if mimetype == "text/plain" { + return fmt.Errorf("cannot remove text/plain parts") + } + for i, part := range c.textParts { + if part.MimeType != mimetype { + continue + } + c.textParts = append(c.textParts[:i], c.textParts[i+1:]...) + c.resetReview() + return nil + } + return fmt.Errorf("%s part not found", mimetype) +} + func (c *Composer) AddTemplate(template string, data interface{}) error { if template == "" { return nil @@ -1361,7 +1381,15 @@ func newReviewMessage(composer *Composer, err error) *reviewMessage { grid.AddChild(ui.NewText("text/plain", uiConfig.GetStyle(config.STYLE_DEFAULT))).At(i, 0) i += 1 for _, p := range composer.textParts { - grid.AddChild(ui.NewText(p.MimeType, uiConfig.GetStyle(config.STYLE_DEFAULT))).At(i, 0) + err := composer.updateMultipart(p) + if err != nil { + msg := fmt.Sprintf("%s error: %s", p.MimeType, err) + grid.AddChild(ui.NewText(msg, + uiConfig.GetStyle(config.STYLE_ERROR))).At(i, 0) + } else { + grid.AddChild(ui.NewText(p.MimeType, + uiConfig.GetStyle(config.STYLE_DEFAULT))).At(i, 0) + } i += 1 } @@ -1374,6 +1402,42 @@ func newReviewMessage(composer *Composer, err error) *reviewMessage { } } +func (c *Composer) updateMultipart(p *lib.Part) error { + command, found := c.aerc.Config().Converters[p.MimeType] + if !found { + // unreachable + return fmt.Errorf("no command defined for mime/type") + } + // reset part body to avoid it leaving outdated if the command fails + p.Data = nil + err := c.reloadEmail() + if err != nil { + return errors.Wrap(err, "reloadEmail") + } + body, err := io.ReadAll(c.email) + if err != nil { + return errors.Wrap(err, "io.ReadAll") + } + cmd := exec.Command("sh", "-c", command) + cmd.Stdin = bytes.NewReader(body) + out, err := cmd.Output() + if err != nil { + var stderr string + var ee *exec.ExitError + if errors.As(err, &ee) { + // append the first 30 chars of stderr if any + stderr = strings.Trim(string(ee.Stderr), " \t\n\r") + stderr = strings.ReplaceAll(stderr, "\n", "; ") + if stderr != "" { + stderr = fmt.Sprintf(": %.30s", stderr) + } + } + return fmt.Errorf("%s: %w%s", command, err, stderr) + } + p.Data = out + return nil +} + func (rm *reviewMessage) Invalidate() { ui.Invalidate() } -- cgit