forked from Silverfish/proton-bridge
396 lines
10 KiB
Go
396 lines
10 KiB
Go
// Copyright (c) 2022 Proton AG
|
|
//
|
|
// This file is part of Proton Mail Bridge.
|
|
//
|
|
// Proton Mail 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.
|
|
//
|
|
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package message
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"io"
|
|
"net/textproto"
|
|
"strconv"
|
|
"strings"
|
|
|
|
pmmime "github.com/ProtonMail/proton-bridge/v2/pkg/mime"
|
|
"github.com/emersion/go-imap"
|
|
"github.com/pkg/errors"
|
|
"github.com/vmihailenco/msgpack/v5"
|
|
)
|
|
|
|
// BodyStructure is used to parse an email into MIME sections and then generate
|
|
// body structure for IMAP server.
|
|
type BodyStructure map[string]*SectionInfo
|
|
|
|
// SectionInfo is used to hold data about parts of each section.
|
|
type SectionInfo struct {
|
|
Header []byte
|
|
Start, BSize, Size, Lines int
|
|
reader io.Reader
|
|
isHeaderReadFinished bool
|
|
}
|
|
|
|
// Read will also count the final size of section.
|
|
func (si *SectionInfo) Read(p []byte) (n int, err error) {
|
|
n, err = si.reader.Read(p)
|
|
si.Size += n
|
|
si.Lines += bytes.Count(p, []byte("\n"))
|
|
|
|
si.readHeader(p)
|
|
return
|
|
}
|
|
|
|
// readHeader appends read data to Header until empty line is found.
|
|
func (si *SectionInfo) readHeader(p []byte) {
|
|
if si.isHeaderReadFinished {
|
|
return
|
|
}
|
|
|
|
si.Header = append(si.Header, p...)
|
|
|
|
if i := bytes.Index(si.Header, []byte("\n\r\n")); i > 0 {
|
|
si.Header = si.Header[:i+3]
|
|
si.isHeaderReadFinished = true
|
|
return
|
|
}
|
|
|
|
// textproto works also with simple line ending so we should be liberal
|
|
// as well.
|
|
if i := bytes.Index(si.Header, []byte("\n\n")); i > 0 {
|
|
si.Header = si.Header[:i+2]
|
|
si.isHeaderReadFinished = true
|
|
}
|
|
}
|
|
|
|
// GetMIMEHeader parses bytes and return MIME header.
|
|
func (si *SectionInfo) GetMIMEHeader() (textproto.MIMEHeader, error) {
|
|
return textproto.NewReader(bufio.NewReader(bytes.NewReader(si.Header))).ReadMIMEHeader()
|
|
}
|
|
|
|
func NewBodyStructure(reader io.Reader) (structure *BodyStructure, err error) {
|
|
structure = &BodyStructure{}
|
|
err = structure.Parse(reader)
|
|
return
|
|
}
|
|
|
|
// DeserializeBodyStructure will create new structure from msgpack bytes.
|
|
func DeserializeBodyStructure(raw []byte) (*BodyStructure, error) {
|
|
bs := &BodyStructure{}
|
|
err := msgpack.Unmarshal(raw, bs)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "cannot deserialize bodystructure")
|
|
}
|
|
return bs, err
|
|
}
|
|
|
|
// Serialize will write msgpack bytes.
|
|
func (bs *BodyStructure) Serialize() ([]byte, error) {
|
|
data, err := msgpack.Marshal(bs)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "cannot serialize bodystructure")
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// Parse will read the mail and create all body structures.
|
|
func (bs *BodyStructure) Parse(r io.Reader) error {
|
|
return bs.parseAllChildSections(r, []int{}, 0)
|
|
}
|
|
|
|
func (bs *BodyStructure) parseAllChildSections(r io.Reader, currentPath []int, start int) (err error) { //nolint:funlen
|
|
info := &SectionInfo{
|
|
Start: start,
|
|
Size: 0,
|
|
BSize: 0,
|
|
Lines: 0,
|
|
reader: r,
|
|
}
|
|
|
|
bufInfo := bufio.NewReader(info)
|
|
tp := textproto.NewReader(bufInfo)
|
|
|
|
tpHeader, err := tp.ReadMIMEHeader()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
bodyInfo := &SectionInfo{reader: tp.R}
|
|
bodyReader := bufio.NewReader(bodyInfo)
|
|
|
|
mediaType, params, _ := pmmime.ParseMediaType(tpHeader.Get("Content-Type"))
|
|
|
|
// If multipart, call getAllParts, else read to count lines.
|
|
if (strings.HasPrefix(mediaType, "multipart/") || mediaType == rfc822Message) && params["boundary"] != "" {
|
|
nextPath := getChildPath(currentPath)
|
|
|
|
var br *boundaryReader
|
|
br, err = newBoundaryReader(bodyReader, params["boundary"])
|
|
// New reader seeks first boundary.
|
|
if err != nil {
|
|
// Return also EOF.
|
|
return
|
|
}
|
|
|
|
for err == nil {
|
|
start += br.skipped
|
|
part := &bytes.Buffer{}
|
|
err = br.writeNextPartTo(part)
|
|
if err != nil {
|
|
break
|
|
}
|
|
err = bs.parseAllChildSections(part, nextPath, start)
|
|
part.Reset()
|
|
nextPath[len(nextPath)-1]++
|
|
}
|
|
br.reader = nil
|
|
|
|
if err == io.EOF {
|
|
err = nil
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
} else {
|
|
// Count length.
|
|
_, _ = bodyReader.WriteTo(io.Discard)
|
|
}
|
|
|
|
// Clear all buffers.
|
|
bodyReader = nil //nolint:wastedassign // just to be sure we clear garbage collector
|
|
bodyInfo.reader = nil
|
|
tp.R = nil
|
|
tp = nil //nolint:wastedassign // just to be sure we clear garbage collector
|
|
bufInfo = nil //nolint:ineffassign,wastedassign // just to be sure we clear garbage collector
|
|
info.reader = nil
|
|
|
|
// Store boundaries.
|
|
info.BSize = bodyInfo.Size
|
|
path := stringPathFromInts(currentPath)
|
|
(*bs)[path] = info
|
|
|
|
// Fix start of subsections.
|
|
newPath := getChildPath(currentPath)
|
|
shift := info.Size - info.BSize
|
|
subInfo, err := bs.getInfo(newPath)
|
|
|
|
// If it has subparts.
|
|
for err == nil {
|
|
subInfo.Start += shift
|
|
|
|
// Level down.
|
|
subInfo, err = bs.getInfo(append(newPath, 1))
|
|
if err == nil {
|
|
newPath = append(newPath, 1)
|
|
continue
|
|
}
|
|
|
|
// Next.
|
|
newPath[len(newPath)-1]++
|
|
subInfo, err = bs.getInfo(newPath)
|
|
if err == nil {
|
|
continue
|
|
}
|
|
|
|
// Level up.
|
|
for {
|
|
newPath = newPath[:len(newPath)-1]
|
|
if len(newPath) > 0 {
|
|
newPath[len(newPath)-1]++
|
|
subInfo, err = bs.getInfo(newPath)
|
|
if err != nil {
|
|
err = nil
|
|
continue
|
|
}
|
|
}
|
|
break
|
|
}
|
|
|
|
// The end.
|
|
if len(newPath) == 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getChildPath will return the first child path of parent path.
|
|
// NOTE: Return value can be used to iterate over parts so it is necessary to
|
|
// copy parrent values in order to not rewrite values in parent.
|
|
func getChildPath(parent []int) []int {
|
|
// append alloc inline is the fasted way to copy
|
|
return append(append(make([]int, 0, len(parent)+1), parent...), 1)
|
|
}
|
|
|
|
func stringPathFromInts(ints []int) (ret string) {
|
|
for i, n := range ints {
|
|
if i != 0 {
|
|
ret += "."
|
|
}
|
|
ret += strconv.Itoa(n)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (bs *BodyStructure) hasInfo(sectionPath []int) bool {
|
|
_, err := bs.getInfo(sectionPath)
|
|
return err == nil
|
|
}
|
|
|
|
func (bs *BodyStructure) getInfoCheckSection(sectionPath []int) (sectionInfo *SectionInfo, err error) {
|
|
if len(*bs) == 1 && len(sectionPath) == 1 && sectionPath[0] == 1 {
|
|
sectionPath = []int{}
|
|
}
|
|
return bs.getInfo(sectionPath)
|
|
}
|
|
|
|
func (bs *BodyStructure) getInfo(sectionPath []int) (sectionInfo *SectionInfo, err error) {
|
|
path := stringPathFromInts(sectionPath)
|
|
sectionInfo, ok := (*bs)[path]
|
|
if !ok {
|
|
err = errors.New("wrong section " + path)
|
|
}
|
|
return
|
|
}
|
|
|
|
// GetSection returns bytes of section including MIME header.
|
|
func (bs *BodyStructure) GetSection(wholeMail io.ReadSeeker, sectionPath []int) (section []byte, err error) {
|
|
info, err := bs.getInfoCheckSection(sectionPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
return goToOffsetAndReadNBytes(wholeMail, info.Start, info.Size)
|
|
}
|
|
|
|
// GetSectionContent returns bytes of section content (excluding MIME header).
|
|
func (bs *BodyStructure) GetSectionContent(wholeMail io.ReadSeeker, sectionPath []int) (section []byte, err error) {
|
|
info, err := bs.getInfoCheckSection(sectionPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
return goToOffsetAndReadNBytes(wholeMail, info.Start+info.Size-info.BSize, info.BSize)
|
|
}
|
|
|
|
// GetMailHeader returns the main header of mail.
|
|
func (bs *BodyStructure) GetMailHeader() (header textproto.MIMEHeader, err error) {
|
|
return bs.GetSectionHeader([]int{})
|
|
}
|
|
|
|
// GetMailHeaderBytes returns the bytes with main mail header.
|
|
// Warning: It can contain extra lines.
|
|
func (bs *BodyStructure) GetMailHeaderBytes() (header []byte, err error) {
|
|
return bs.GetSectionHeaderBytes([]int{})
|
|
}
|
|
|
|
func goToOffsetAndReadNBytes(wholeMail io.ReadSeeker, offset, length int) ([]byte, error) {
|
|
if length == 0 {
|
|
return []byte{}, nil
|
|
}
|
|
if length < 0 {
|
|
return nil, errors.New("requested negative length")
|
|
}
|
|
if offset > 0 {
|
|
if _, err := wholeMail.Seek(int64(offset), io.SeekStart); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
out := make([]byte, length)
|
|
_, err := wholeMail.Read(out)
|
|
return out, err
|
|
}
|
|
|
|
// GetSectionHeader returns the mime header of specified section.
|
|
func (bs *BodyStructure) GetSectionHeader(sectionPath []int) (textproto.MIMEHeader, error) {
|
|
info, err := bs.getInfoCheckSection(sectionPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return info.GetMIMEHeader()
|
|
}
|
|
|
|
// GetSectionHeaderBytes returns raw header bytes of specified section.
|
|
func (bs *BodyStructure) GetSectionHeaderBytes(sectionPath []int) ([]byte, error) {
|
|
info, err := bs.getInfoCheckSection(sectionPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return info.Header, nil
|
|
}
|
|
|
|
// IMAPBodyStructure will prepare imap bodystructure recurently for given part.
|
|
// Use empty path to create whole email structure.
|
|
func (bs *BodyStructure) IMAPBodyStructure(currentPart []int) (imapBS *imap.BodyStructure, err error) {
|
|
var info *SectionInfo
|
|
if info, err = bs.getInfo(currentPart); err != nil {
|
|
return
|
|
}
|
|
|
|
tpHeader, err := info.GetMIMEHeader()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
mediaType, params, _ := pmmime.ParseMediaType(tpHeader.Get("Content-Type"))
|
|
|
|
mediaTypeSep := strings.Split(mediaType, "/")
|
|
|
|
// If it is empty or missing it will not crash.
|
|
mediaTypeSep = append(mediaTypeSep, "")
|
|
|
|
imapBS = &imap.BodyStructure{
|
|
MIMEType: mediaTypeSep[0],
|
|
MIMESubType: mediaTypeSep[1],
|
|
Params: params,
|
|
Size: uint32(info.BSize),
|
|
Lines: uint32(info.Lines),
|
|
}
|
|
|
|
if val := tpHeader.Get("Content-ID"); val != "" {
|
|
imapBS.Id = val
|
|
}
|
|
|
|
if val := tpHeader.Get("Content-Transfer-Encoding"); val != "" {
|
|
imapBS.Encoding = val
|
|
}
|
|
|
|
if val := tpHeader.Get("Content-Description"); val != "" {
|
|
imapBS.Description = val
|
|
}
|
|
|
|
if val := tpHeader.Get("Content-Disposition"); val != "" {
|
|
imapBS.Disposition = val
|
|
}
|
|
|
|
nextPart := append(currentPart, 1) //nolint:gocritic
|
|
for {
|
|
if !bs.hasInfo(nextPart) {
|
|
break
|
|
}
|
|
var subStruct *imap.BodyStructure
|
|
subStruct, err = bs.IMAPBodyStructure(nextPart)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if imapBS.Parts == nil {
|
|
imapBS.Parts = []*imap.BodyStructure{}
|
|
}
|
|
imapBS.Parts = append(imapBS.Parts, subStruct)
|
|
nextPart[len(nextPart)-1]++
|
|
}
|
|
|
|
return imapBS, nil
|
|
}
|