You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
199 lines
7.1 KiB
199 lines
7.1 KiB
// Copyright (c) 2022 Proton Technologies AG |
|
// |
|
// This file is part of ProtonMail Bridge. |
|
// |
|
// ProtonMail Bridge is free software: you can redistribute it and/or modify |
|
// it under the terms of the GNU General Public License as published by |
|
// the Free Software Foundation, either version 3 of the License, or |
|
// (at your option) any later version. |
|
// |
|
// ProtonMail Bridge is distributed in the hope that it will be useful, |
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
// GNU General Public License for more details. |
|
// |
|
// You should have received a copy of the GNU General Public License |
|
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. |
|
|
|
package imap |
|
|
|
import ( |
|
"bytes" |
|
|
|
"github.com/emersion/go-imap" |
|
"github.com/ljanyst/peroxide/pkg/message" |
|
"github.com/ljanyst/peroxide/pkg/store" |
|
"github.com/pkg/errors" |
|
) |
|
|
|
func (im *imapMailbox) getMessage(storeMessage *store.Message, items []imap.FetchItem) (msg *imap.Message, err error) { |
|
msglog := im.log.WithField("msgID", storeMessage.ID()) |
|
msglog.Trace("Getting message") |
|
|
|
seqNum, err := storeMessage.SequenceNumber() |
|
if err != nil { |
|
return |
|
} |
|
|
|
m := storeMessage.Message() |
|
|
|
msg = imap.NewMessage(seqNum, items) |
|
for _, item := range items { |
|
switch item { |
|
case imap.FetchEnvelope: |
|
// No need to retrieve full header here. API header |
|
// contains enough information to build the envelope. |
|
msg.Envelope = message.GetEnvelope(m, storeMessage.GetMIMEHeaderFast()) |
|
case imap.FetchBody, imap.FetchBodyStructure: |
|
structure, err := im.getBodyStructure(storeMessage) |
|
if err != nil { |
|
return nil, err |
|
} |
|
if msg.BodyStructure, err = structure.IMAPBodyStructure([]int{}); err != nil { |
|
return nil, err |
|
} |
|
case imap.FetchFlags: |
|
msg.Flags = message.GetFlags(m) |
|
if storeMessage.IsMarkedDeleted() { |
|
msg.Flags = append(msg.Flags, imap.DeletedFlag) |
|
} |
|
case imap.FetchInternalDate: |
|
// Apple Mail crashes fetching messages with date older than 1970. |
|
// There is no point having message older than RFC itself, it's not possible. |
|
msg.InternalDate = message.SanitizeMessageDate(m.Time) |
|
case imap.FetchRFC822Size: |
|
size, err := storeMessage.GetRFC822Size() |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
msg.Size = size |
|
case imap.FetchUid: |
|
if msg.Uid, err = storeMessage.UID(); err != nil { |
|
return nil, err |
|
} |
|
case imap.FetchAll, imap.FetchFast, imap.FetchFull, imap.FetchRFC822, imap.FetchRFC822Header, imap.FetchRFC822Text: |
|
fallthrough // this is list of defined items by go-imap, but items can be also sections generated from requests |
|
default: |
|
if err = im.getLiteralForSection(item, msg, storeMessage); err != nil { |
|
return |
|
} |
|
} |
|
} |
|
|
|
return msg, err |
|
} |
|
|
|
func (im *imapMailbox) getLiteralForSection(itemSection imap.FetchItem, msg *imap.Message, storeMessage *store.Message) error { |
|
section, err := imap.ParseBodySectionName(itemSection) |
|
if err != nil { |
|
log.WithError(err).Warn("Failed to parse body section name; part will be skipped") |
|
return nil //nolint[nilerr] ignore error |
|
} |
|
|
|
var literal imap.Literal |
|
if literal, err = im.getMessageBodySection(storeMessage, section); err != nil { |
|
return err |
|
} |
|
|
|
msg.Body[section] = literal |
|
return nil |
|
} |
|
|
|
// getBodyStructure returns the cached body structure or it will build the message, |
|
// save the structure in DB and then returns the structure after build. |
|
// |
|
// Apple Mail requests body structure for all messages irregularly. We cache |
|
// bodystructure in local database in order to not re-download all messages |
|
// from server. |
|
func (im *imapMailbox) getBodyStructure(storeMessage *store.Message) (bs *message.BodyStructure, err error) { |
|
bs, err = storeMessage.GetBodyStructure() |
|
if err != nil { |
|
im.log.WithError(err).Debug("Fail to retrieve bodystructure from database") |
|
} |
|
if bs == nil { |
|
// We are sure the body structure is not a problem right now. |
|
// Clients might do first fetch body structure so we couldn't |
|
// be sure if seeing 1st or 2nd sync is all right or not. |
|
// Therefore, it's better to exclude first body structure fetch |
|
// from the counting and see build count as real message build. |
|
if bs, _, err = im.getBodyAndStructure(storeMessage); err != nil { |
|
return |
|
} |
|
} |
|
return |
|
} |
|
|
|
func (im *imapMailbox) getBodyAndStructure(storeMessage *store.Message) (*message.BodyStructure, *bytes.Reader, error) { |
|
rfc822, err := storeMessage.GetRFC822() |
|
if err != nil { |
|
return nil, nil, err |
|
} |
|
|
|
structure, err := storeMessage.GetBodyStructure() |
|
if err != nil { |
|
return nil, nil, err |
|
} |
|
|
|
return structure, bytes.NewReader(rfc822), nil |
|
} |
|
|
|
// This will download message (or read from cache) and pick up the section, |
|
// extract data (header,body, both) and trim the output if needed. |
|
// |
|
// In order to speed up (avoid download and decryptions) we |
|
// cache the header. If a mail header was requested and DB |
|
// contains full header (it means it was already built once) |
|
// the DB header can be used without downloading and decrypting. |
|
// Otherwise header is incomplete and clients would have issues |
|
// e.g. AppleMail expects `text/plain` in HTML mails. |
|
// |
|
// For all other cases it is necessary to download and decrypt the message |
|
// and drop the header which was obtained from cache. The header will |
|
// will be stored in DB once successfully built. Check `getBodyAndStructure`. |
|
func (im *imapMailbox) getMessageBodySection(storeMessage *store.Message, section *imap.BodySectionName) (imap.Literal, error) { |
|
var header []byte |
|
var response []byte |
|
|
|
im.log.WithField("msgID", storeMessage.ID()).Trace("Getting message body") |
|
|
|
isMainHeaderRequested := len(section.Path) == 0 && section.Specifier == imap.HeaderSpecifier |
|
if isMainHeaderRequested && storeMessage.IsFullHeaderCached() { |
|
var err error |
|
if header, err = storeMessage.GetHeader(); err != nil { |
|
return nil, err |
|
} |
|
} else { |
|
structure, bodyReader, err := im.getBodyAndStructure(storeMessage) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
switch { |
|
case section.Specifier == imap.EntireSpecifier && len(section.Path) == 0: |
|
// An empty section specification refers to the entire message, including the header. |
|
response, err = structure.GetSection(bodyReader, section.Path) |
|
case section.Specifier == imap.TextSpecifier || (section.Specifier == imap.EntireSpecifier && len(section.Path) != 0): |
|
// The TEXT specifier refers to the content of the message (or section), omitting the [RFC-2822] header. |
|
// Non-empty section with no specifier (imap.EntireSpecifier) refers to section content without header. |
|
response, err = structure.GetSectionContent(bodyReader, section.Path) |
|
case section.Specifier == imap.MIMESpecifier: // The MIME part specifier refers to the [MIME-IMB] header for this part. |
|
fallthrough |
|
case section.Specifier == imap.HeaderSpecifier: |
|
header, err = structure.GetSectionHeaderBytes(section.Path) |
|
default: |
|
err = errors.New("Unknown specifier " + string(section.Specifier)) |
|
} |
|
|
|
if err != nil { |
|
return nil, err |
|
} |
|
} |
|
|
|
if header != nil { |
|
response = filterHeader(header, section) |
|
} |
|
|
|
// Trim any output if requested. |
|
return bytes.NewBuffer(section.ExtractPartial(response)), nil |
|
}
|
|
|