aboutsummaryrefslogblamecommitdiffstats
path: root/worker/jmap/send.go
blob: 4b033d2ec6d5b67ddb33e9a0060c8610e079d44e (plain) (tree)















































































































































                                                                                   
package jmap

import (
	"fmt"
	"io"
	"strings"

	"git.sr.ht/~rjarry/aerc/log"
	"git.sr.ht/~rjarry/aerc/worker/types"
	"git.sr.ht/~rockorager/go-jmap"
	"git.sr.ht/~rockorager/go-jmap/mail/email"
	"git.sr.ht/~rockorager/go-jmap/mail/emailsubmission"
	"git.sr.ht/~rockorager/go-jmap/mail/mailbox"
	"github.com/emersion/go-message/mail"
)

func (w *JMAPWorker) handleStartSend(msg *types.StartSendingMessage) error {
	reader, writer := io.Pipe()
	send := &jmapSendWriter{writer: writer, done: make(chan error)}

	w.w.PostMessage(&types.MessageWriter{
		Message: types.RespondTo(msg),
		Writer:  send,
	}, nil)

	go func() {
		defer log.PanicHandler()
		defer close(send.done)

		identity, err := w.getSenderIdentity(msg.Header)
		if err != nil {
			send.done <- err
			return
		}

		blob, err := w.Upload(reader)
		if err != nil {
			send.done <- err
			return
		}

		var req jmap.Request

		// Import the blob into drafts
		req.Invoke(&email.Import{
			Account: w.accountId,
			Emails: map[string]*email.EmailImport{
				"aerc": {
					BlobID: blob.ID,
					MailboxIDs: map[jmap.ID]bool{
						w.roles[mailbox.RoleDrafts]: true,
					},
					Keywords: map[string]bool{
						"$draft": true,
						"$seen":  true,
					},
				},
			},
		})

		// Create the submission
		req.Invoke(&emailsubmission.Set{
			Account: w.accountId,
			Create: map[jmap.ID]*emailsubmission.EmailSubmission{
				"sub": {
					IdentityID: identity,
					EmailID:    "#aerc",
				},
			},
			OnSuccessUpdateEmail: map[jmap.ID]jmap.Patch{
				"#sub": {
					"keywords/$draft":               nil,
					w.rolePatch(mailbox.RoleSent):   true,
					w.rolePatch(mailbox.RoleDrafts): nil,
				},
			},
		})

		resp, err := w.Do(&req)
		if err != nil {
			send.done <- err
			return
		}

		for _, inv := range resp.Responses {
			switch r := inv.Args.(type) {
			case *email.ImportResponse:
				if err, ok := r.NotCreated["aerc"]; ok {
					send.done <- wrapSetError(err)
					return
				}
			case *emailsubmission.SetResponse:
				if err, ok := r.NotCreated["sub"]; ok {
					send.done <- wrapSetError(err)
					return
				}
			case *jmap.MethodError:
				send.done <- wrapMethodError(r)
				return
			}
		}
	}()

	return nil
}

type jmapSendWriter struct {
	writer *io.PipeWriter
	done   chan error
}

func (w *jmapSendWriter) Write(data []byte) (int, error) {
	return w.writer.Write(data)
}

func (w *jmapSendWriter) Close() error {
	writeErr := w.writer.Close()
	sendErr := <-w.done
	if writeErr != nil {
		return writeErr
	}
	return sendErr
}

func (w *JMAPWorker) getSenderIdentity(header *mail.Header) (jmap.ID, error) {
	from, err := header.AddressList("from")
	if err != nil {
		return "", fmt.Errorf("msg.Header.AddressList: %w", err)
	}
	if len(from) != 1 {
		return "", fmt.Errorf("no from header in message")
	}
	name, domain, _ := strings.Cut(from[0].Address, "@")
	for _, ident := range w.identities {
		n, d, _ := strings.Cut(ident.Email, "@")
		switch {
		case n == name && d == domain:
			fallthrough
		case n == "*" && d == domain:
			return ident.ID, nil
		}
	}
	return "", fmt.Errorf("no identity found for address: %s@%s", name, domain)
}