mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 12:46:46 +00:00
414 lines
8.9 KiB
Go
414 lines
8.9 KiB
Go
// Copyright (c) 2020 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 message
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/textproto"
|
|
"strconv"
|
|
"strings"
|
|
|
|
pmmime "github.com/ProtonMail/proton-bridge/pkg/mime"
|
|
"github.com/emersion/go-imap"
|
|
)
|
|
|
|
type sectionInfo struct {
|
|
header textproto.MIMEHeader
|
|
start, bsize, size, lines int
|
|
reader io.Reader
|
|
}
|
|
|
|
// Count and read.
|
|
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"))
|
|
return
|
|
}
|
|
|
|
type boundaryReader struct {
|
|
reader *bufio.Reader
|
|
|
|
closed, first bool
|
|
skipped int
|
|
|
|
nl []byte // "\r\n" or "\n" (set after seeing first boundary line)
|
|
nlDashBoundary []byte // nl + "--boundary"
|
|
dashBoundaryDash []byte // "--boundary--"
|
|
dashBoundary []byte // "--boundary"
|
|
}
|
|
|
|
func newBoundaryReader(r *bufio.Reader, boundary string) (br *boundaryReader, err error) {
|
|
b := []byte("\r\n--" + boundary + "--")
|
|
br = &boundaryReader{
|
|
reader: r,
|
|
closed: false,
|
|
first: true,
|
|
nl: b[:2],
|
|
nlDashBoundary: b[:len(b)-2],
|
|
dashBoundaryDash: b[2:],
|
|
dashBoundary: b[2 : len(b)-2],
|
|
}
|
|
err = br.WriteNextPartTo(nil)
|
|
return
|
|
}
|
|
|
|
func skipLWSPChar(b []byte) []byte {
|
|
for len(b) > 0 && (b[0] == ' ' || b[0] == '\t') {
|
|
b = b[1:]
|
|
}
|
|
return b
|
|
}
|
|
|
|
func (br *boundaryReader) isFinalBoundary(line []byte) bool {
|
|
if !bytes.HasPrefix(line, br.dashBoundaryDash) {
|
|
return false
|
|
}
|
|
rest := line[len(br.dashBoundaryDash):]
|
|
rest = skipLWSPChar(rest)
|
|
return len(rest) == 0 || bytes.Equal(rest, br.nl)
|
|
}
|
|
|
|
func (br *boundaryReader) isBoundaryDelimiterLine(line []byte) (ret bool) {
|
|
if !bytes.HasPrefix(line, br.dashBoundary) {
|
|
return false
|
|
}
|
|
rest := line[len(br.dashBoundary):]
|
|
rest = skipLWSPChar(rest)
|
|
|
|
if br.first && len(rest) == 1 && rest[0] == '\n' {
|
|
br.nl = br.nl[1:]
|
|
br.nlDashBoundary = br.nlDashBoundary[1:]
|
|
}
|
|
return bytes.Equal(rest, br.nl)
|
|
}
|
|
|
|
func (br *boundaryReader) WriteNextPartTo(part io.Writer) (err error) {
|
|
if br.closed {
|
|
return io.EOF
|
|
}
|
|
|
|
var line, slice []byte
|
|
br.skipped = 0
|
|
|
|
for {
|
|
slice, err = br.reader.ReadSlice('\n')
|
|
line = append(line, slice...)
|
|
if err == bufio.ErrBufferFull {
|
|
continue
|
|
}
|
|
|
|
br.skipped += len(line)
|
|
|
|
if err == io.EOF && br.isFinalBoundary(line) {
|
|
err = nil
|
|
br.closed = true
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if br.isBoundaryDelimiterLine(line) {
|
|
br.first = false
|
|
return
|
|
}
|
|
|
|
if br.isFinalBoundary(line) {
|
|
br.closed = true
|
|
return
|
|
}
|
|
|
|
if part != nil {
|
|
if _, err = part.Write(line); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
line = []byte{}
|
|
}
|
|
}
|
|
|
|
type BodyStructure map[string]*sectionInfo
|
|
|
|
func NewBodyStructure(reader io.Reader) (structure *BodyStructure, err error) {
|
|
structure = &BodyStructure{}
|
|
err = structure.Parse(reader)
|
|
return
|
|
}
|
|
|
|
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 := §ionInfo{
|
|
start: start,
|
|
size: 0,
|
|
bsize: 0,
|
|
lines: 0,
|
|
reader: r,
|
|
}
|
|
|
|
bufInfo := bufio.NewReader(info)
|
|
tp := textproto.NewReader(bufInfo)
|
|
|
|
if info.header, err = tp.ReadMIMEHeader(); err != nil {
|
|
return
|
|
}
|
|
|
|
bodyInfo := §ionInfo{reader: tp.R}
|
|
bodyReader := bufio.NewReader(bodyInfo)
|
|
|
|
mediaType, params, _ := pmmime.ParseMediaType(info.header.Get("Content-Type"))
|
|
|
|
// If multipart, call getAllParts, else read to count lines.
|
|
if (strings.HasPrefix(mediaType, "multipart/") || mediaType == "message/rfc822") && params["boundary"] != "" {
|
|
newPath := append(currentPath, 1)
|
|
|
|
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, newPath, start)
|
|
part.Reset()
|
|
newPath[len(newPath)-1]++
|
|
}
|
|
br.reader = nil
|
|
|
|
if err == io.EOF {
|
|
err = nil
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
} else {
|
|
// Count length.
|
|
_, _ = bodyReader.WriteTo(ioutil.Discard)
|
|
}
|
|
|
|
// Clear all buffers.
|
|
bodyReader = nil
|
|
bodyInfo.reader = nil
|
|
tp.R = nil
|
|
tp = nil
|
|
bufInfo = nil // nolint
|
|
info.reader = nil
|
|
|
|
// Store boundaries.
|
|
info.bsize = bodyInfo.size
|
|
path := stringPathFromInts(currentPath)
|
|
(*bs)[path] = info
|
|
|
|
// Fix start of subsections.
|
|
newPath := append(currentPath, 1)
|
|
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
|
|
}
|
|
|
|
func stringPathFromInts(ints []int) (ret string) {
|
|
for i, n := range ints {
|
|
if i != 0 {
|
|
ret += "."
|
|
}
|
|
ret += strconv.Itoa(n)
|
|
}
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (bs *BodyStructure) GetSection(wholeMail io.ReadSeeker, sectionPath []int) (section []byte, err error) {
|
|
info, err := bs.getInfo(sectionPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if _, err = wholeMail.Seek(int64(info.start), io.SeekStart); err != nil {
|
|
return
|
|
}
|
|
section = make([]byte, info.size)
|
|
_, err = wholeMail.Read(section)
|
|
return
|
|
}
|
|
|
|
func (bs *BodyStructure) GetSectionContent(wholeMail io.ReadSeeker, sectionPath []int) (section []byte, err error) {
|
|
info, err := bs.getInfo(sectionPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if _, err = wholeMail.Seek(int64(info.start+info.size-info.bsize), io.SeekStart); err != nil {
|
|
return
|
|
}
|
|
section = make([]byte, info.bsize)
|
|
_, err = wholeMail.Read(section)
|
|
return
|
|
|
|
/* This is slow:
|
|
sectionBuf, err := bs.GetSection(wholeMail, sectionPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
tp := textproto.NewReader(bufio.NewReader(buf))
|
|
if _, err = tp.ReadMIMEHeader(); err != nil {
|
|
return err
|
|
}
|
|
|
|
sectionBuf = &bytes.Buffer{}
|
|
_, err = io.Copy(sectionBuf, tp.R)
|
|
return
|
|
*/
|
|
}
|
|
|
|
func (bs *BodyStructure) GetSectionHeader(sectionPath []int) (header textproto.MIMEHeader, err error) {
|
|
info, err := bs.getInfo(sectionPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
header = info.header
|
|
return
|
|
}
|
|
|
|
func (bs *BodyStructure) Size() uint32 {
|
|
info, err := bs.getInfo([]int{})
|
|
if err != nil {
|
|
return uint32(0)
|
|
}
|
|
return uint32(info.size)
|
|
}
|
|
|
|
func (bs *BodyStructure) IMAPBodyStructure(currentPart []int) (imapBS *imap.BodyStructure, err error) {
|
|
var info *sectionInfo
|
|
if info, err = bs.getInfo(currentPart); err != nil {
|
|
return
|
|
}
|
|
|
|
mediaType, params, _ := pmmime.ParseMediaType(info.header.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 := info.header.Get("Content-ID"); val != "" {
|
|
imapBS.Id = val
|
|
}
|
|
|
|
if val := info.header.Get("Content-Transfer-Encoding"); val != "" {
|
|
imapBS.Encoding = val
|
|
}
|
|
|
|
if val := info.header.Get("Content-Description"); val != "" {
|
|
imapBS.Description = val
|
|
}
|
|
|
|
if val := info.header.Get("Content-Disposition"); val != "" {
|
|
imapBS.Disposition = val
|
|
}
|
|
|
|
nextPart := append(currentPart, 1)
|
|
for {
|
|
if _, err := bs.getInfo(nextPart); err != nil {
|
|
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
|
|
}
|