Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ type Provider interface {
// EmailReader fetches emails and their content.
type EmailReader interface {
FetchEmails(ctx context.Context, folder string, limit, offset uint32) ([]Email, error)
FetchEmailBody(ctx context.Context, folder string, uid uint32) (string, []Attachment, error)
// FetchEmailBody returns the chosen body, its MIME type ("text/html" or
// "text/plain"; empty when unknown), parsed attachments, and any error.
FetchEmailBody(ctx context.Context, folder string, uid uint32) (string, string, []Attachment, error)
FetchAttachment(ctx context.Context, folder string, uid uint32, partID, encoding string) ([]byte, error)
}

Expand Down
8 changes: 4 additions & 4 deletions backend/imap/imap.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ func (p *Provider) FetchEmails(_ context.Context, folder string, limit, offset u
return toBackendEmails(emails), nil
}

func (p *Provider) FetchEmailBody(_ context.Context, folder string, uid uint32) (string, []backend.Attachment, error) {
body, atts, err := fetcher.FetchEmailBodyFromMailbox(p.account, folder, uid)
func (p *Provider) FetchEmailBody(_ context.Context, folder string, uid uint32) (string, string, []backend.Attachment, error) {
body, mimeType, atts, err := fetcher.FetchEmailBodyFromMailbox(p.account, folder, uid)
if err != nil {
return "", nil, err
return "", "", nil, err
}
return body, toBackendAttachments(atts), nil
return body, mimeType, toBackendAttachments(atts), nil
}

func (p *Provider) FetchAttachment(_ context.Context, folder string, uid uint32, partID, encoding string) ([]byte, error) {
Expand Down
14 changes: 8 additions & 6 deletions backend/jmap/jmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,10 +276,10 @@ func searchLimit(query backend.SearchQuery) uint32 {
return 100
}

func (p *Provider) FetchEmailBody(_ context.Context, _ string, uid uint32) (string, []backend.Attachment, error) {
func (p *Provider) FetchEmailBody(_ context.Context, _ string, uid uint32) (string, string, []backend.Attachment, error) {
jmapID, err := p.lookupJMAPID(uid)
if err != nil {
return "", nil, err
return "", "", nil, err
}

req := &jmapclient.Request{}
Expand All @@ -297,25 +297,27 @@ func (p *Provider) FetchEmailBody(_ context.Context, _ string, uid uint32) (stri

resp, err := p.client.Do(req)
if err != nil {
return "", nil, fmt.Errorf("jmap body: %w", err)
return "", "", nil, fmt.Errorf("jmap body: %w", err)
}

for _, inv := range resp.Responses {
if r, ok := inv.Args.(*email.GetResponse); ok && len(r.List) > 0 {
eml := r.List[0]

// Get body text (prefer HTML)
var body string
var body, mimeType string
for _, part := range eml.HTMLBody {
if val, ok := eml.BodyValues[part.PartID]; ok {
body = val.Value
mimeType = "text/html"
break
}
}
if body == "" {
for _, part := range eml.TextBody {
if val, ok := eml.BodyValues[part.PartID]; ok {
body = val.Value
mimeType = "text/plain"
break
}
}
Expand All @@ -336,11 +338,11 @@ func (p *Provider) FetchEmailBody(_ context.Context, _ string, uid uint32) (stri
atts = append(atts, a)
}

return body, atts, nil
return body, mimeType, atts, nil
}
}

return "", nil, fmt.Errorf("jmap: email not found")
return "", "", nil, fmt.Errorf("jmap: email not found")
}

func (p *Provider) FetchAttachment(_ context.Context, _ string, _ uint32, partID, _ string) ([]byte, error) {
Expand Down
22 changes: 12 additions & 10 deletions backend/pop3/pop3.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,21 +131,21 @@ func (p *Provider) FetchEmails(_ context.Context, _ string, limit, offset uint32
return emails, nil
}

func (p *Provider) FetchEmailBody(_ context.Context, _ string, uid uint32) (string, []backend.Attachment, error) {
func (p *Provider) FetchEmailBody(_ context.Context, _ string, uid uint32) (string, string, []backend.Attachment, error) {
conn, err := p.connect()
if err != nil {
return "", nil, err
return "", "", nil, err
}
defer conn.Quit()

msgID, err := p.findMessageByUID(conn, uid)
if err != nil {
return "", nil, err
return "", "", nil, err
}

raw, err := conn.RetrRaw(msgID)
if err != nil {
return "", nil, fmt.Errorf("pop3 retr: %w", err)
return "", "", nil, fmt.Errorf("pop3 retr: %w", err)
}

return parseMessageBody(raw)
Expand Down Expand Up @@ -352,15 +352,17 @@ func entityToEmail(header *message.Header, msgInfo pop3client.MessageID, account
}

// parseMessageBody extracts the body text and attachments from a raw message.
func parseMessageBody(r io.Reader) (string, []backend.Attachment, error) {
func parseMessageBody(r io.Reader) (string, string, []backend.Attachment, error) {
mr, err := gomail.CreateReader(r)
if err != nil {
// Not a multipart message — read body directly
// Not a multipart message — read body directly. We don't know the
// content type at this layer; surface empty so the renderer falls
// back to its legacy markdown→HTML path.
body, err := io.ReadAll(r)
if err != nil {
return "", nil, err
return "", "", nil, err
}
return string(body), nil, nil
return string(body), "", nil, nil
}

var bodyText string
Expand Down Expand Up @@ -411,9 +413,9 @@ func parseMessageBody(r io.Reader) (string, []backend.Attachment, error) {
}

if htmlBody != "" {
return htmlBody, attachments, nil
return htmlBody, "text/html", attachments, nil
}
return bodyText, attachments, nil
return bodyText, "text/plain", attachments, nil
}

// findAttachmentData walks a raw message to find attachment data by partID.
Expand Down
1 change: 1 addition & 0 deletions config/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ type CachedEmailBody struct {
UID uint32 `json:"uid"`
AccountID string `json:"account_id"`
Body string `json:"body"`
BodyMIMEType string `json:"body_mime_type,omitempty"` // empty for cache rows written before MIME-type tracking; renderer falls back to legacy markdown→HTML pre-pass
Attachments []CachedAttachment `json:"attachments,omitempty"`
CachedAt time.Time `json:"cached_at"`
LastAccessedAt time.Time `json:"last_accessed_at"`
Expand Down
7 changes: 4 additions & 3 deletions daemon/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ func (d *Daemon) handleFetchEmailBody(conn *daemonrpc.Conn, req *daemonrpc.Reque
ctx, cancel := context.WithTimeout(context.Background(), fetchTimeout)
defer cancel()

body, attachments, err := p.FetchEmailBody(ctx, params.Folder, params.UID)
body, mimeType, attachments, err := p.FetchEmailBody(ctx, params.Folder, params.UID)
if err != nil {
conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
return
Expand All @@ -172,8 +172,9 @@ func (d *Daemon) handleFetchEmailBody(conn *daemonrpc.Conn, req *daemonrpc.Reque
}

conn.SendResponse(req.ID, daemonrpc.FetchEmailBodyResult{
Body: body,
Attachments: attInfos,
Body: body,
BodyMIMEType: mimeType,
Attachments: attInfos,
})
}

Expand Down
14 changes: 8 additions & 6 deletions daemonclient/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import (
// TUI and CLI use this interface — they don't care which mode is active.
type Service interface {
FetchEmails(accountID, folder string, limit, offset uint32) ([]backend.Email, error)
FetchEmailBody(accountID, folder string, uid uint32) (string, []backend.Attachment, error)
// FetchEmailBody returns body, MIME type ("text/html"|"text/plain"|""),
// attachments, and any error.
FetchEmailBody(accountID, folder string, uid uint32) (string, string, []backend.Attachment, error)
DeleteEmails(accountID, folder string, uids []uint32) error
ArchiveEmails(accountID, folder string, uids []uint32) error
MoveEmails(accountID string, uids []uint32, src, dst string) error
Expand Down Expand Up @@ -102,15 +104,15 @@ func (s *daemonService) FetchEmails(accountID, folder string, limit, offset uint
return emails, err
}

func (s *daemonService) FetchEmailBody(accountID, folder string, uid uint32) (string, []backend.Attachment, error) {
func (s *daemonService) FetchEmailBody(accountID, folder string, uid uint32) (string, string, []backend.Attachment, error) {
var result daemonrpc.FetchEmailBodyResult
err := s.client.Call(daemonrpc.MethodFetchEmailBody, daemonrpc.FetchEmailBodyParams{
AccountID: accountID,
Folder: folder,
UID: uid,
}, &result)
if err != nil {
return "", nil, err
return "", "", nil, err
}

var attachments []backend.Attachment
Expand All @@ -122,7 +124,7 @@ func (s *daemonService) FetchEmailBody(accountID, folder string, uid uint32) (st
MIMEType: a.MIMEType,
})
}
return result.Body, attachments, nil
return result.Body, result.BodyMIMEType, attachments, nil
}

func (s *daemonService) DeleteEmails(accountID, folder string, uids []uint32) error {
Expand Down Expand Up @@ -251,10 +253,10 @@ func (s *directService) FetchEmails(accountID, folder string, limit, offset uint
return p.FetchEmails(context.Background(), folder, limit, offset)
}

func (s *directService) FetchEmailBody(accountID, folder string, uid uint32) (string, []backend.Attachment, error) {
func (s *directService) FetchEmailBody(accountID, folder string, uid uint32) (string, string, []backend.Attachment, error) {
p, err := s.getProvider(accountID)
if err != nil {
return "", nil, err
return "", "", nil, err
}
return p.FetchEmailBody(context.Background(), folder, uid)
}
Expand Down
5 changes: 3 additions & 2 deletions daemonrpc/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,9 @@ type FetchEmailBodyParams struct {
}

type FetchEmailBodyResult struct {
Body string `json:"body"`
Attachments []AttachmentInfo `json:"attachments"`
Body string `json:"body"`
BodyMIMEType string `json:"body_mime_type,omitempty"`
Attachments []AttachmentInfo `json:"attachments"`
}

type AttachmentInfo struct {
Expand Down
Loading
Loading