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.
265 lines
8.4 KiB
265 lines
8.4 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 ( |
|
"bufio" |
|
"bytes" |
|
"fmt" |
|
"io/ioutil" |
|
"net/mail" |
|
"strings" |
|
"time" |
|
|
|
"github.com/ProtonMail/gopenpgp/v2/crypto" |
|
"github.com/emersion/go-imap" |
|
"github.com/emersion/go-message/textproto" |
|
"github.com/ljanyst/peroxide/pkg/imap/uidplus" |
|
"github.com/ljanyst/peroxide/pkg/message" |
|
"github.com/ljanyst/peroxide/pkg/pmapi" |
|
"github.com/ljanyst/peroxide/pkg/store" |
|
"github.com/pkg/errors" |
|
) |
|
|
|
// 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 { |
|
return im.logCommand(func() error { |
|
return im.createMessage(flags, date, body) |
|
}, "APPEND", flags, date) |
|
} |
|
|
|
func (im *imapMailbox) createMessage(imapFlags []string, date time.Time, r imap.Literal) error { //nolint[funlen] |
|
// NOTE: Is this lock meant to be here? |
|
im.user.appendExpungeLock.Lock() |
|
defer im.user.appendExpungeLock.Unlock() |
|
|
|
body, err := ioutil.ReadAll(r) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
addr := im.storeAddress.APIAddress() |
|
if addr == nil { |
|
return errors.New("no available address for encryption") |
|
} |
|
|
|
kr, err := im.user.client().KeyRingForAddressID(addr.ID) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
if im.storeMailbox.LabelID() == pmapi.DraftLabel { |
|
return im.createDraftMessage(kr, addr.Email, body) |
|
} |
|
|
|
if im.storeMailbox.LabelID() == pmapi.SentLabel { |
|
m, _, _, _, err := message.Parse(bytes.NewReader(body)) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
if m.Sender == nil { |
|
m.Sender = &mail.Address{Address: addr.Email} |
|
} |
|
|
|
if user, err := im.user.backend.usersMgr.GetUser(pmapi.SanitizeEmail(m.Sender.Address)); err == nil && user.ID() == im.storeUser.UserID() { |
|
logEntry := im.log.WithField("sender", m.Sender).WithField("extID", m.Header.Get("Message-Id")).WithField("date", date) |
|
|
|
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}) |
|
} |
|
|
|
logEntry.Info("No matching UID, continuing APPEND to Sent") |
|
} |
|
} |
|
|
|
hdr, err := textproto.ReadHeader(bufio.NewReader(bytes.NewReader(body))) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
// Avoid appending a message which is already on the server. Apply the new label instead. |
|
// This always happens with Outlook because it uses APPEND instead of COPY. |
|
internalID := hdr.Get("X-Pm-Internal-Id") |
|
|
|
// In case there is a mail client which corrupts headers, try "References" too. |
|
if internalID == "" { |
|
if references := strings.Fields(hdr.Get("References")); len(references) > 0 { |
|
if match := pmapi.RxInternalReferenceFormat.FindStringSubmatch(references[len(references)-1]); len(match) == 2 { |
|
internalID = match[1] |
|
} |
|
} |
|
} |
|
|
|
if internalID != "" { |
|
if msg, err := im.storeMailbox.GetMessage(internalID); err == nil { |
|
return im.labelExistingMessage(msg) |
|
} |
|
} |
|
return im.importMessage(kr, hdr, body, imapFlags, date) |
|
} |
|
|
|
func (im *imapMailbox) createDraftMessage(kr *crypto.KeyRing, email string, body []byte) error { |
|
im.log.Info("Creating draft message") |
|
|
|
m, _, _, readers, err := message.Parse(bytes.NewReader(body)) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
if m.Sender == nil { |
|
m.Sender = &mail.Address{} |
|
} |
|
|
|
m.Sender.Address = pmapi.ConstructAddress(m.Sender.Address, email) |
|
|
|
draft, _, err := im.user.storeUser.CreateDraft(kr, m, readers, "", "", "") |
|
if err != nil { |
|
return errors.Wrap(err, "failed to create draft") |
|
} |
|
|
|
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), im.storeMailbox.GetUIDList([]string{draft.ID})) |
|
} |
|
|
|
func findMailboxForAddress(address *store.Address, labelID string) (*store.Mailbox, error) { |
|
for _, mailBox := range address.ListMailboxes() { |
|
if mailBox.LabelID() == labelID { |
|
return mailBox, nil |
|
} |
|
} |
|
return nil, fmt.Errorf("could not find %v label in mailbox for user %v", labelID, |
|
address.AddressString()) |
|
} |
|
|
|
func (im *imapMailbox) labelExistingMessage(msg *store.Message) error { //nolint[funlen] |
|
im.log.Info("Labelling existing message") |
|
|
|
// IMAP clients can move message to local folder (setting \Deleted flag) |
|
// and then move it back (IMAP client does not remember the message, |
|
// so instead removing the flag it imports duplicate message). |
|
// Regular IMAP server would keep the message twice and later EXPUNGE would |
|
// not delete the message (EXPUNGE would delete the original message and |
|
// the new duplicate one would stay). API detects duplicates; therefore |
|
// we need to remove \Deleted flag if IMAP client re-imports. |
|
if msg.IsMarkedDeleted() { |
|
if err := im.storeMailbox.MarkMessagesUndeleted([]string{msg.ID()}); err != nil { |
|
log.WithError(err).Error("Failed to undelete re-imported message") |
|
} |
|
} |
|
|
|
// Outlook Uses APPEND instead of COPY. There is no need to copy to All Mail because messages are already there. |
|
// If the message is copied from Spam or Trash, it must be moved otherwise we will have data loss. |
|
// If the message is moved from any folder, the moment when expunge happens on source we will move message trash unless we move it to archive. |
|
// If the message is already in Archive we should not call API at all. |
|
// Otherwise the message is already in All mail, Return OK. |
|
var storeMBox = im.storeMailbox |
|
if pmapi.AllMailLabel == storeMBox.LabelID() { |
|
if msg.Message().HasLabelID(pmapi.ArchiveLabel) { |
|
return uidplus.AppendResponse(storeMBox.UIDValidity(), storeMBox.GetUIDList([]string{msg.ID()})) |
|
} |
|
var err error |
|
storeMBox, err = findMailboxForAddress(im.storeAddress, pmapi.ArchiveLabel) |
|
if err != nil { |
|
return err |
|
} |
|
} |
|
|
|
if err := storeMBox.LabelMessages([]string{msg.ID()}); err != nil { |
|
return err |
|
} |
|
|
|
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), im.storeMailbox.GetUIDList([]string{msg.ID()})) |
|
} |
|
|
|
func (im *imapMailbox) importMessage(kr *crypto.KeyRing, hdr textproto.Header, body []byte, imapFlags []string, date time.Time) error { //nolint[funlen] |
|
im.log.Info("Importing external message") |
|
|
|
var ( |
|
seen bool |
|
flags int64 |
|
labelIDs []string |
|
time int64 |
|
) |
|
|
|
if hdr.Get("received") == "" { |
|
flags = pmapi.FlagSent |
|
} else { |
|
flags = pmapi.FlagReceived |
|
} |
|
|
|
for _, flag := range imapFlags { |
|
switch flag { |
|
case imap.DraftFlag: |
|
flags &= ^pmapi.FlagSent |
|
flags &= ^pmapi.FlagReceived |
|
|
|
case imap.SeenFlag: |
|
seen = true |
|
|
|
case imap.FlaggedFlag: |
|
labelIDs = append(labelIDs, pmapi.StarredLabel) |
|
|
|
case imap.AnsweredFlag: |
|
flags |= pmapi.FlagReplied |
|
} |
|
} |
|
|
|
if !date.IsZero() { |
|
time = date.Unix() |
|
} |
|
|
|
enc, err := message.EncryptRFC822(kr, bytes.NewReader(body)) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
var targetMailbox = im.storeMailbox |
|
if targetMailbox.LabelID() == pmapi.AllMailLabel { |
|
// Importing mail in directly into All Mail is not allowed. Instead we redirect the import to Archive |
|
// The mail will automatically appear in All mail. The appends response still reports that the mail was |
|
// successfully APPEND to All Mail. |
|
targetMailbox, err = findMailboxForAddress(im.storeAddress, pmapi.ArchiveLabel) |
|
if err != nil { |
|
return err |
|
} |
|
} |
|
|
|
messageID, err := targetMailbox.ImportMessage(enc, seen, labelIDs, flags, time) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
msg, err := targetMailbox.GetMessage(messageID) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
if msg.IsMarkedDeleted() { |
|
if err := targetMailbox.MarkMessagesUndeleted([]string{messageID}); err != nil { |
|
log.WithError(err).Error("Failed to undelete re-imported message") |
|
} |
|
} |
|
|
|
return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), im.storeMailbox.GetUIDList([]string{messageID})) |
|
}
|
|
|