// 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 imap

import (
	"bytes"
	"encoding/base64"
	"fmt"
	"io"
	"io/ioutil"
	"mime/multipart"
	"net/mail"
	"net/textproto"
	"regexp"
	"sort"
	"strings"
	"time"

	pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
	"github.com/ProtonMail/proton-bridge/internal/imap/cache"
	"github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
	"github.com/ProtonMail/proton-bridge/pkg/message"
	"github.com/ProtonMail/proton-bridge/pkg/parallel"
	"github.com/ProtonMail/proton-bridge/pkg/pmapi"
	"github.com/emersion/go-imap"
	"github.com/emersion/go-textwrapper"
	"github.com/hashicorp/go-multierror"
	enmime "github.com/jhillyerd/enmime"
	"github.com/pkg/errors"
	openpgperrors "golang.org/x/crypto/openpgp/errors"
)

type doNotCacheError struct{ e error }

func (dnc *doNotCacheError) Error() string { return dnc.e.Error() }
func (dnc *doNotCacheError) add(err error) { dnc.e = multierror.Append(dnc.e, err) }
func (dnc *doNotCacheError) errorOrNil() error {
	if dnc == nil {
		return nil
	}

	if dnc.e != nil {
		return dnc
	}

	return nil
}

// CreateMessage appends a new message to this mailbox. The \Recent flag will
// be added regardless of whether flags is empty or not. If date is nil, the
// current time will be used.
//
// If the Backend implements Updater, it must notify the client immediately
// via a mailbox update.
func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.Literal) error { // nolint[funlen]
	// Called from go-imap in goroutines - we need to handle panics for each function.
	defer im.panicHandler.HandlePanic()

	m, _, _, readers, err := message.Parse(body, "", "")
	if err != nil {
		return err
	}

	addr := im.storeAddress.APIAddress()
	if addr == nil {
		return errors.New("no available address for encryption")
	}
	m.AddressID = addr.ID
	kr := addr.KeyRing()

	// Handle imported messages which have no "Sender" address.
	// This sometimes occurs with outlook which reports errors as imported emails or for drafts.
	if m.Sender == nil {
		im.log.Warning("Append: Missing email sender. Will use main address")
		m.Sender = &mail.Address{
			Name:    "",
			Address: addr.Email,
		}
	}

	// "Drafts" needs to call special API routes.
	// Clients always append the whole message again and remove the old one.
	if im.storeMailbox.LabelID() == pmapi.DraftLabel {
		// Sender address needs to be sanitised (drafts need to match cases exactly).
		m.Sender.Address = pmapi.ConstructAddress(m.Sender.Address, addr.Email)

		draft, _, err := im.user.storeUser.CreateDraft(kr, m, readers, "", "", "")
		if err != nil {
			return errors.Wrap(err, "failed to create draft")
		}

		targetSeq := im.storeMailbox.GetUIDList([]string{draft.ID})
		return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
	}

	// We need to make sure this is an import, and not a sent message from this account
	// (sent messages from the account will be added by the event loop).
	if im.storeMailbox.LabelID() == pmapi.SentLabel {
		sanitizedSender := pmapi.SanitizeEmail(m.Sender.Address)

		// Check whether this message was sent by a bridge user.
		user, err := im.user.backend.bridge.GetUser(sanitizedSender)
		if err == nil && user.ID() == im.storeUser.UserID() {
			logEntry := im.log.WithField("addr", sanitizedSender).WithField("extID", m.Header.Get("Message-Id"))

			// If we find the message in the store already, we can skip importing it.
			if foundUID := im.storeMailbox.GetUIDByHeader(&m.Header); foundUID != uint32(0) {
				logEntry.Info("Ignoring APPEND of duplicate to Sent folder")
				return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), &uidplus.OrderedSeq{foundUID})
			}

			// We didn't find the message in the store, so we are currently sending it.
			logEntry.WithField("time", date).Info("No matching UID, continuing APPEND to Sent")
		}

		// This is an APPEND to the Sent folder, so we will set the sent flag
		m.Flags |= pmapi.FlagSent
	}

	message.ParseFlags(m, flags)
	if !date.IsZero() {
		m.Time = date.Unix()
	}

	internalID := m.Header.Get("X-Pm-Internal-Id")
	references := m.Header.Get("References")
	referenceList := strings.Fields(references)

	if len(referenceList) > 0 {
		lastReference := referenceList[len(referenceList)-1]
		// In case we are using a mail client which corrupts headers, try "References" too.
		re := regexp.MustCompile("<[a-zA-Z0-9-_=]*@protonmail.internalid>")
		match := re.FindString(lastReference)
		if match != "" {
			internalID = match[1 : len(match)-len("@protonmail.internalid>")]
		}
	}

	// Avoid appending a message which is already on the server. Apply the new
	// label instead. This sometimes happens which Outlook (it uses APPEND instead of COPY).
	if internalID != "" {
		// Check to see if this belongs to a different address in split mode or another ProtonMail account.
		msg, err := im.storeMailbox.GetMessage(internalID)
		if err == nil && (im.user.user.IsCombinedAddressMode() || (im.storeAddress.AddressID() == msg.Message().AddressID)) {
			IDs := []string{internalID}

			err = im.storeMailbox.LabelMessages(IDs)
			if err != nil {
				return err
			}

			targetSeq := im.storeMailbox.GetUIDList([]string{m.ID})
			return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
		}
	}

	im.log.Info("Importing external message")
	if err := im.importMessage(m, readers, kr); err != nil {
		im.log.Error("Import failed: ", err)
		return err
	}

	targetSeq := im.storeMailbox.GetUIDList([]string{m.ID})
	return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq)
}

func (im *imapMailbox) importMessage(m *pmapi.Message, readers []io.Reader, kr *pmcrypto.KeyRing) (err error) { // nolint[funlen]
	b := &bytes.Buffer{}

	// Overwrite content for main header for import.
	// Even if message has just simple body we should upload as multipart/mixed.
	// Each part has encrypted body and header reflects the original header.
	mainHeader := message.GetHeader(m)
	mainHeader.Set("Content-Type", "multipart/mixed; boundary="+message.GetBoundary(m))
	mainHeader.Del("Content-Disposition")
	mainHeader.Del("Content-Transfer-Encoding")
	if err = writeHeader(b, mainHeader); err != nil {
		return
	}
	mw := multipart.NewWriter(b)
	if err = mw.SetBoundary(message.GetBoundary(m)); err != nil {
		return
	}

	// Write the body part.
	bodyHeader := make(textproto.MIMEHeader)
	bodyHeader.Set("Content-Type", m.MIMEType+"; charset=utf-8")
	bodyHeader.Set("Content-Disposition", "inline")
	bodyHeader.Set("Content-Transfer-Encoding", "7bit")

	var p io.Writer
	if p, err = mw.CreatePart(bodyHeader); err != nil {
		return
	}
	// First, encrypt the message body.
	if err = m.Encrypt(kr, kr); err != nil {
		return err
	}
	if _, err := io.WriteString(p, m.Body); err != nil {
		return err
	}

	// Write the attachments parts.
	for i := 0; i < len(m.Attachments); i++ {
		att := m.Attachments[i]
		r := readers[i]
		h := message.GetAttachmentHeader(att)
		if p, err = mw.CreatePart(h); err != nil {
			return
		}
		// Create line wrapper writer.
		ww := textwrapper.NewRFC822(p)

		// Create base64 writer.
		bw := base64.NewEncoder(base64.StdEncoding, ww)

		data, err := ioutil.ReadAll(r)
		if err != nil {
			return err
		}

		// Create encrypted writer.
		pgpMessage, err := kr.Encrypt(pmcrypto.NewPlainMessage(data), nil)
		if err != nil {
			return err
		}
		if _, err := bw.Write(pgpMessage.GetBinary()); err != nil {
			return err
		}
		if err := bw.Close(); err != nil {
			return err
		}
	}

	if err := mw.Close(); err != nil {
		return err
	}

	labels := []string{}
	for _, l := range m.LabelIDs {
		if l == pmapi.StarredLabel {
			labels = append(labels, pmapi.StarredLabel)
		}
	}

	return im.storeMailbox.ImportMessage(m, b.Bytes(), labels)
}

func (im *imapMailbox) getMessage(storeMessage storeMessageProvider, items []string) (msg *imap.Message, err error) {
	im.log.WithField("msgID", storeMessage.ID()).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.EnvelopeMsgAttr:
			msg.Envelope = message.GetEnvelope(m)
		case imap.BodyMsgAttr, imap.BodyStructureMsgAttr:
			var structure *message.BodyStructure
			if structure, _, err = im.getBodyStructure(storeMessage); err != nil {
				return
			}
			if msg.BodyStructure, err = structure.IMAPBodyStructure([]int{}); err != nil {
				return
			}
		case imap.FlagsMsgAttr:
			msg.Flags = message.GetFlags(m)
		case imap.InternalDateMsgAttr:
			msg.InternalDate = time.Unix(m.Time, 0)
		case imap.SizeMsgAttr:
			// Size attribute on the server counts encrypted data. The value is cleared
			// on our part and we need to compute "real" size of decrypted data.
			if m.Size <= 0 {
				if _, _, err = im.getBodyStructure(storeMessage); err != nil {
					return
				}
			}
			msg.Size = uint32(m.Size)
		case imap.UidMsgAttr:
			msg.Uid, err = storeMessage.UID()
			if err != nil {
				return nil, err
			}
		default:
			s := item

			var section *imap.BodySectionName
			if section, err = imap.NewBodySectionName(s); err != nil {
				err = nil // Ignore error
				break
			}

			var literal imap.Literal
			if literal, err = im.getMessageBodySection(storeMessage, section); err != nil {
				return
			}

			msg.Body[section] = literal
		}
	}

	return msg, err
}

func (im *imapMailbox) getBodyStructure(storeMessage storeMessageProvider) (
	structure *message.BodyStructure,
	bodyReader *bytes.Reader, err error,
) {
	m := storeMessage.Message()
	id := im.storeUser.UserID() + m.ID
	cache.BuildLock(id)
	if bodyReader, structure = cache.LoadMail(id); bodyReader.Len() == 0 || structure == nil {
		var body []byte
		structure, body, err = im.buildMessage(m)
		if err == nil && structure != nil && len(body) > 0 {
			m.Size = int64(len(body))
			if err := storeMessage.SetSize(m.Size); err != nil {
				im.log.WithError(err).
					WithField("newSize", m.Size).
					WithField("msgID", m.ID).
					Warn("Cannot update size while building")
			}
			if err := storeMessage.SetContentTypeAndHeader(m.MIMEType, m.Header); err != nil {
				im.log.WithError(err).
					WithField("msgID", m.ID).
					Warn("Cannot update header while building")
			}
			// Drafts can change and we don't want to cache them.
			if !isMessageInDraftFolder(m) {
				cache.SaveMail(id, body, structure)
			}
			bodyReader = bytes.NewReader(body)
		}
		if _, ok := err.(*doNotCacheError); ok {
			im.log.WithField("msgID", m.ID).Errorf("do not cache message: %v", err)
			err = nil
			bodyReader = bytes.NewReader(body)
		}
	}
	cache.BuildUnlock(id)
	return structure, bodyReader, err
}

func isMessageInDraftFolder(m *pmapi.Message) bool {
	for _, labelID := range m.LabelIDs {
		if labelID == pmapi.DraftLabel {
			return true
		}
	}
	return false
}

// This will download message (or read from cache) and pick up the section,
// extract data (header,body, both) and trim the output if needed.
func (im *imapMailbox) getMessageBodySection(storeMessage storeMessageProvider, section *imap.BodySectionName) (literal imap.Literal, err error) { // nolint[funlen]
	var (
		structure  *message.BodyStructure
		bodyReader *bytes.Reader
		header     textproto.MIMEHeader
		response   []byte
	)

	im.log.WithField("msgID", storeMessage.ID()).Trace("Getting message body")

	m := storeMessage.Message()

	if len(section.Path) == 0 && section.Specifier == imap.HeaderSpecifier {
		// We can extract message header without decrypting.
		header = message.GetHeader(m)
		// We need to ensure we use the correct content-type,
		// otherwise AppleMail expects `text/plain` in HTML mails.
		if header.Get("Content-Type") == "" {
			if err = im.fetchMessage(m); err != nil {
				return
			}
			if _, err = im.setMessageContentType(m); err != nil {
				return
			}
			if err = storeMessage.SetContentTypeAndHeader(m.MIMEType, m.Header); err != nil {
				return
			}
			header = message.GetHeader(m)
		}
	} else {
		// The rest of cases need download and decrypt.
		structure, bodyReader, err = im.getBodyStructure(storeMessage)
		if err != nil {
			return
		}

		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.GetSectionHeader(section.Path)
		default:
			err = errors.New("Unknown specifier " + section.Specifier)
		}
	}

	if err != nil {
		return
	}

	// Filter header. Options are: all fields, only selected fields, all fields except selected.
	if header != nil {
		// remove fields
		if len(section.Fields) != 0 && section.NotFields {
			for _, field := range section.Fields {
				header.Del(field)
			}
		}

		fields := make([]string, 0, len(header))
		if len(section.Fields) == 0 || section.NotFields { // add all and sort
			for f := range header {
				fields = append(fields, f)
			}
			sort.Strings(fields)
		} else { // add only requested (in requested order)
			for _, f := range section.Fields {
				fields = append(fields, textproto.CanonicalMIMEHeaderKey(f))
			}
		}

		headerBuf := &bytes.Buffer{}
		for _, canonical := range fields {
			if values, ok := header[canonical]; !ok {
				continue
			} else {
				for _, val := range values {
					fmt.Fprintf(headerBuf, "%s: %s\r\n", canonical, val)
				}
			}
		}
		response = headerBuf.Bytes()
	}

	// Trim any output if requested.
	literal = bytes.NewBuffer(section.ExtractPartial(response))
	return literal, nil
}

func (im *imapMailbox) fetchMessage(m *pmapi.Message) (err error) {
	im.log.Trace("Fetching message")

	complete, err := im.storeMailbox.FetchMessage(m.ID)
	if err != nil {
		im.log.WithError(err).Error("Could not get message from store")
		return
	}

	*m = *complete.Message()

	return
}

func (im *imapMailbox) customMessage(m *pmapi.Message, err error, attachBody bool) {
	// Assuming quoted-printable.
	origBody := strings.Replace(m.Body, "=", "=3D", -1)
	m.Body = "Content-Type: text/html\r\n"
	m.Body = "\n<html><head></head><body style=3D\"font-family: Arial,'Helvetica Neue',Helvetica,sans-serif; font-size: 14px; \">\n"
	m.Body += "<div style=3D\"color:#555; background-color:#cf9696; padding:20px; border-radius: 4px;\" >\n<strong>Decryption error</strong><br/>Decryption of this message's encrypted content failed.<pre>\n"
	m.Body += err.Error()
	m.Body += "\n</pre></div>\n"

	if attachBody {
		m.Body += "<div style=3D\"color:#333; background-color:#f4f4f4;  border: 1px solid #acb0bf; border-radius: 2px; padding:1rem; margin:1rem 0; font-family:monospace; font-size: 1em;\" ><pre>\n"
		m.Body += origBody
		m.Body += "\n</pre></div>\n"
	}

	m.Body += "</body></html>"
	m.MIMEType = "text/html"

	// NOTE: we need to set header in custom message header, so we check that is non-nil.
	if m.Header == nil {
		m.Header = make(mail.Header)
	}
}

func (im *imapMailbox) writeMessageBody(w io.Writer, m *pmapi.Message) (err error) {
	im.log.Trace("Writing message body")

	if m.Body == "" {
		im.log.Trace("While writing message body, noticed message body is null, need to fetch")
		if err = im.fetchMessage(m); err != nil {
			return
		}
	}

	kr := im.user.client.KeyRingForAddressID(m.AddressID)
	err = message.WriteBody(w, kr, m)
	if err != nil {
		im.customMessage(m, err, true)
		_, _ = io.WriteString(w, m.Body)
		err = nil
	}

	return
}

func (im *imapMailbox) writeAndParseMIMEBody(m *pmapi.Message) (mime *enmime.Envelope, err error) { //nolint[unused]
	b := &bytes.Buffer{}
	if err = im.writeMessageBody(b, m); err != nil {
		return
	}

	mime, err = enmime.ReadEnvelope(b)

	return
}

func (im *imapMailbox) writeAttachmentBody(w io.Writer, m *pmapi.Message, att *pmapi.Attachment) (err error) {
	// Retrieve encrypted attachment.
	r, err := im.user.client.GetAttachment(att.ID)
	if err != nil {
		return
	}
	defer r.Close() //nolint[errcheck]

	kr := im.user.client.KeyRingForAddressID(m.AddressID)
	if err = message.WriteAttachmentBody(w, kr, m, att, r); err != nil {
		// Returning an error here makes certain mail clients behave badly,
		// trying to retrieve the message again and again.
		im.log.Warn("Cannot write attachment body: ", err)
		err = nil
	}
	return
}

func (im *imapMailbox) writeRelatedPart(p io.Writer, m *pmapi.Message, inlines []*pmapi.Attachment) (err error) {
	related := multipart.NewWriter(p)

	_ = related.SetBoundary(message.GetRelatedBoundary(m))

	buf := &bytes.Buffer{}
	if err = im.writeMessageBody(buf, m); err != nil {
		return
	}

	// Write the body part.
	h := message.GetBodyHeader(m)

	if p, err = related.CreatePart(h); err != nil {
		return
	}

	_, _ = buf.WriteTo(p)

	for _, inline := range inlines {
		buf = &bytes.Buffer{}
		if err = im.writeAttachmentBody(buf, m, inline); err != nil {
			return
		}

		h := message.GetAttachmentHeader(inline)
		if p, err = related.CreatePart(h); err != nil {
			return
		}
		_, _ = buf.WriteTo(p)
	}

	_ = related.Close()
	return nil
}

const (
	noMultipart      = iota // only body
	simpleMultipart         // body + attachment or inline
	complexMultipart        // mixed, rfc822, alternatives, ...
)

func (im *imapMailbox) setMessageContentType(m *pmapi.Message) (multipartType int, err error) {
	if m.MIMEType == "" {
		err = fmt.Errorf("trying to set Content-Type without MIME TYPE")
		return
	}
	// message.MIMEType can have just three values from our server:
	// * `text/html` (refers to body type, but might contain attachments and inlines)
	// * `text/plain` (refers to body type, but might contain attachments and inlines)
	// * `multipart/mixed` (refers to external message with multipart structure)
	// The proper header content fields must be set and saved to DB based MIMEType and content.
	multipartType = noMultipart
	if m.MIMEType == pmapi.ContentTypeMultipartMixed {
		multipartType = complexMultipart
	} else if m.NumAttachments != 0 {
		multipartType = simpleMultipart
	}

	h := textproto.MIMEHeader(m.Header)
	if multipartType == noMultipart {
		message.SetBodyContentFields(&h, m)
	} else {
		h.Set("Content-Type",
			fmt.Sprintf("%s; boundary=%s", "multipart/mixed", message.GetBoundary(m)),
		)
	}
	m.Header = mail.Header(h)

	return
}

// buildMessage from PM to IMAP.
func (im *imapMailbox) buildMessage(m *pmapi.Message) (structure *message.BodyStructure, msgBody []byte, err error) {
	im.log.Trace("Building message")

	var errNoCache doNotCacheError

	// If fetch or decryption fails we need to change the MIMEType (in customMessage).
	err = im.fetchMessage(m)
	if err != nil {
		return
	}

	kr := im.user.client.KeyRingForAddressID(m.AddressID)
	errDecrypt := m.Decrypt(kr)

	if errDecrypt != nil && errDecrypt != openpgperrors.ErrSignatureExpired {
		errNoCache.add(errDecrypt)
		im.customMessage(m, errDecrypt, true)
	}

	// Inner function can fail even when message is decrypted.
	// #1048 For example we have problem with double-encrypted messages
	// which seems as still encrypted and we try them to decrypt again
	// and that fails. For any building error is better to return custom
	// message than error because it will not be fixed and users would
	// get error message all the time and could not see some messages.
	structure, msgBody, err = im.buildMessageInner(m, kr)
	if err == pmapi.ErrAPINotReachable || err == pmapi.ErrInvalidToken || err == pmapi.ErrUpgradeApplication {
		return nil, nil, err
	} else if err != nil {
		errNoCache.add(err)
		im.customMessage(m, err, true)
		structure, msgBody, err = im.buildMessageInner(m, kr)
		if err != nil {
			return nil, nil, err
		}
	}

	err = errNoCache.errorOrNil()

	return structure, msgBody, err
}

func (im *imapMailbox) buildMessageInner(m *pmapi.Message, kr *pmcrypto.KeyRing) (structure *message.BodyStructure, msgBody []byte, err error) { // nolint[funlen]
	multipartType, err := im.setMessageContentType(m)
	if err != nil {
		return
	}

	tmpBuf := &bytes.Buffer{}
	mainHeader := message.GetHeader(m)
	if err = writeHeader(tmpBuf, mainHeader); err != nil {
		return
	}
	_, _ = io.WriteString(tmpBuf, "\r\n")

	switch multipartType {
	case noMultipart:
		err = message.WriteBody(tmpBuf, kr, m)
		if err != nil {
			return
		}
	case complexMultipart:
		_, _ = io.WriteString(tmpBuf, "\r\n--"+message.GetBoundary(m)+"\r\n")
		err = message.WriteBody(tmpBuf, kr, m)
		if err != nil {
			return
		}
		_, _ = io.WriteString(tmpBuf, "\r\n--"+message.GetBoundary(m)+"--\r\n")
	case simpleMultipart:
		atts, inlines := message.SeparateInlineAttachments(m)
		mw := multipart.NewWriter(tmpBuf)
		_ = mw.SetBoundary(message.GetBoundary(m))

		var partWriter io.Writer

		if len(inlines) > 0 {
			relatedHeader := message.GetRelatedHeader(m)
			if partWriter, err = mw.CreatePart(relatedHeader); err != nil {
				return
			}
			_ = im.writeRelatedPart(partWriter, m, inlines)
		} else {
			buf := &bytes.Buffer{}
			if err = im.writeMessageBody(buf, m); err != nil {
				return
			}

			// Write the body part.
			bodyHeader := message.GetBodyHeader(m)
			if partWriter, err = mw.CreatePart(bodyHeader); err != nil {
				return
			}

			_, _ = buf.WriteTo(partWriter)
		}

		// Write the attachments parts.
		input := make([]interface{}, len(atts))
		for i, att := range atts {
			input[i] = att
		}

		processCallback := func(value interface{}) (interface{}, error) {
			att := value.(*pmapi.Attachment)

			buf := &bytes.Buffer{}
			if err = im.writeAttachmentBody(buf, m, att); err != nil {
				return nil, err
			}
			return buf, nil
		}

		collectCallback := func(idx int, value interface{}) error {
			buf := value.(*bytes.Buffer)
			defer buf.Reset()
			att := atts[idx]

			attachmentHeader := message.GetAttachmentHeader(att)
			if partWriter, err = mw.CreatePart(attachmentHeader); err != nil {
				return err
			}

			_, _ = buf.WriteTo(partWriter)
			return nil
		}

		err = parallel.RunParallel(fetchAttachmentsWorkers, input, processCallback, collectCallback)
		if err != nil {
			return
		}

		_ = mw.Close()
	default:
		fmt.Fprintf(tmpBuf, "\r\n\r\nUknown multipart type: %d\r\n\r\n", multipartType)
	}

	// We need to copy buffer before building body structure.
	msgBody = tmpBuf.Bytes()
	structure, err = message.NewBodyStructure(tmpBuf)
	if err != nil {
		// NOTE: We need to set structure if it fails and is empty.
		if structure == nil {
			structure = &message.BodyStructure{}
		}
	}
	return structure, msgBody, err
}
