// 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 . // NOTE: Comments in this file refer to a specification in a document called "ProtonMail Encryption logic". It will be referred to via abbreviation PMEL. package smtp import ( "bytes" "encoding/base64" "fmt" "io" "math/rand" "mime" "net/mail" "regexp" "strconv" "strings" "time" pmcrypto "github.com/ProtonMail/gopenpgp/crypto" "github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/message" "github.com/ProtonMail/proton-bridge/pkg/pmapi" goSMTPBackend "github.com/emersion/go-smtp" "github.com/pkg/errors" ) type smtpUser struct { panicHandler panicHandler eventListener listener.Listener backend *smtpBackend user bridgeUser client pmapi.Client storeUser storeUserProvider addressID string } // newSMTPUser returns struct implementing go-smtp/session interface. func newSMTPUser( panicHandler panicHandler, eventListener listener.Listener, smtpBackend *smtpBackend, user bridgeUser, addressID string, ) (goSMTPBackend.User, error) { // Using client directly is deprecated. Code should be moved to store. client := user.GetTemporaryPMAPIClient() storeUser := user.GetStore() if storeUser == nil { return nil, errors.New("user database is not initialized") } return &smtpUser{ panicHandler: panicHandler, eventListener: eventListener, backend: smtpBackend, user: user, client: client, storeUser: storeUser, addressID: addressID, }, nil } // Send sends an email from the given address to the given addresses with the given body. func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err error) { //nolint[funlen] // Called from go-smtp in goroutines - we need to handle panics for each function. defer su.panicHandler.HandlePanic() mailSettings, err := su.client.GetMailSettings() if err != nil { return err } var addr *pmapi.Address = su.client.Addresses().ByEmail(from) if addr == nil { err = errors.New("backend: invalid email address: not owned by user") return } kr := addr.KeyRing() var attachedPublicKey string var attachedPublicKeyName string if mailSettings.AttachPublicKey > 0 { attachedPublicKey, err = kr.GetArmoredPublicKey() if err != nil { return err } attachedPublicKeyName = "publickey - " + kr.Identities()[0].Name } message, mimeBody, plainBody, attReaders, err := message.Parse(messageReader, attachedPublicKey, attachedPublicKeyName) if err != nil { return } clearBody := message.Body externalID := message.Header.Get("Message-Id") externalID = strings.Trim(externalID, "<>") draftID, parentID := su.handleReferencesHeader(message) if err = su.handleSenderAndRecipients(message, addr, from, to); err != nil { return err } message.AddressID = addr.ID // Apple Mail Message-Id has to be stored to avoid recovered message after each send. // Before it was done only for Apple Mail, but it should work for any client. Also, the client // is set up from IMAP and no one can be sure that the same client is used for SMTP as well. // Also, user can use more than one client which could break the condition as well. // If there is any problem, condition to Apple Mail only should be returned. // Note: for that, we would need to refactor a little bit and pass the last client name from // the IMAP through the bridge user. message.ExternalID = externalID // If Outlook does not get a response quickly, it will try to send the message again, leading // to sending the same message multiple times. In case we detect the same message is in the // sending queue, we wait a minute to finish the first request. If the message is still being // sent after the timeout, we return an error back to the client. The UX is not the best, // but it's better than sending the message many times. If the message was sent, we simply return // nil to indicate it's OK. sendRecorderMessageHash := su.backend.sendRecorder.getMessageHash(message) isSending, wasSent := su.backend.sendRecorder.isSendingOrSent(su.client, sendRecorderMessageHash) if isSending { log.Debug("Message is in send queue, waiting") time.Sleep(60 * time.Second) isSending, wasSent = su.backend.sendRecorder.isSendingOrSent(su.client, sendRecorderMessageHash) } if isSending { log.Debug("Message is still in send queue, returning error") return errors.New("message is sending") } if wasSent { log.Debug("Message was already sent") return nil } message, atts, err := su.storeUser.CreateDraft(kr, message, attReaders, attachedPublicKey, attachedPublicKeyName, parentID) if err != nil { return } su.backend.sendRecorder.addMessage(sendRecorderMessageHash, message.ID) // We always have to create a new draft even if there already is one, // because clients don't necessarily save the draft before sending, which // can lead to sending the wrong message. Also clients do not necessarily // delete the old draft. if draftID != "" { if err := su.client.DeleteMessages([]string{draftID}); err != nil { log.WithError(err).WithField("draftID", draftID).Warn("Original draft cannot be deleted") } } atts = append(atts, message.Attachments...) // Decrypt attachment keys, because we will need to re-encrypt them with the recipients' public keys. attkeys := make(map[string]*pmcrypto.SymmetricKey) attkeysEncoded := make(map[string]pmapi.AlgoKey) for _, att := range atts { var keyPackets []byte if keyPackets, err = base64.StdEncoding.DecodeString(att.KeyPackets); err != nil { return errors.Wrap(err, "decoding attachment key packets") } if attkeys[att.ID], err = kr.DecryptSessionKey(keyPackets); err != nil { return errors.Wrap(err, "decrypting attachment session key") } attkeysEncoded[att.ID] = pmapi.AlgoKey{ Key: attkeys[att.ID].GetBase64Key(), Algorithm: attkeys[att.ID].Algo, } } plainSharedScheme := 0 htmlSharedScheme := 0 mimeSharedType := 0 plainAddressMap := make(map[string]*pmapi.MessageAddress) htmlAddressMap := make(map[string]*pmapi.MessageAddress) mimeAddressMap := make(map[string]*pmapi.MessageAddress) // PMEL 2. settingsPgpScheme := mailSettings.PGPScheme settingsSign := (mailSettings.Sign > 0) // PMEL 3. composeMode := message.MIMEType var plainKey, htmlKey, mimeKey *pmcrypto.SymmetricKey var plainData, htmlData, mimeData []byte containsUnencryptedRecipients := false for _, email := range to { // PMEL 1. contactEmails, err := su.client.GetContactEmailByEmail(email, 0, 1000) if err != nil { return err } var contactMeta *ContactMetadata var contactKeys []*pmcrypto.KeyRing for _, contactEmail := range contactEmails { if contactEmail.Defaults == 1 { // WARNING: in doc it says _ignore for now, future feature_ continue } contact, err := su.client.GetContactByID(contactEmail.ContactID) if err != nil { return err } decryptedCards, err := su.client.DecryptAndVerifyCards(contact.Cards) if err != nil { return err } contactMeta, err = GetContactMetadataFromVCards(decryptedCards, email) if err != nil { return err } for _, contactRawKey := range contactMeta.Keys { contactKey, err := pmcrypto.ReadKeyRing(bytes.NewBufferString(contactRawKey)) if err != nil { return err } contactKeys = append(contactKeys, contactKey) } break // We take the first hit where Defaults == 0, see "How to find the right contact" of PMEL } // PMEL 4. apiRawKeyList, isInternal, err := su.client.GetPublicKeysForEmail(email) if err != nil { err = fmt.Errorf("backend: cannot get recipients' public keys: %v", err) return err } var apiKeys []*pmcrypto.KeyRing for _, apiRawKey := range apiRawKeyList { var kr *pmcrypto.KeyRing if kr, err = pmcrypto.ReadArmoredKeyRing(strings.NewReader(apiRawKey.PublicKey)); err != nil { return err } apiKeys = append(apiKeys, kr) } sendingInfo, err := generateSendingInfo(su.eventListener, contactMeta, isInternal, composeMode, apiKeys, contactKeys, settingsSign, settingsPgpScheme) if !sendingInfo.Encrypt { containsUnencryptedRecipients = true } if err != nil { return errors.New("error sending to user " + email + ": " + err.Error()) } var signature int if sendingInfo.Sign { signature = pmapi.YesSignature } else { signature = pmapi.NoSignature } if sendingInfo.Scheme == pmapi.PGPMIMEPackage || sendingInfo.Scheme == pmapi.ClearMIMEPackage { if mimeKey == nil { if mimeKey, mimeData, err = encryptSymmetric(kr, mimeBody, true); err != nil { return err } } if sendingInfo.Scheme == pmapi.PGPMIMEPackage { mimeBodyPacket, _, err := createPackets(sendingInfo.PublicKey, mimeKey, map[string]*pmcrypto.SymmetricKey{}) if err != nil { return err } mimeAddressMap[email] = &pmapi.MessageAddress{Type: sendingInfo.Scheme, BodyKeyPacket: mimeBodyPacket, Signature: signature} } else { mimeAddressMap[email] = &pmapi.MessageAddress{Type: sendingInfo.Scheme, Signature: signature} } mimeSharedType |= sendingInfo.Scheme } else { switch sendingInfo.MIMEType { case pmapi.ContentTypePlainText: if plainKey == nil { if plainKey, plainData, err = encryptSymmetric(kr, plainBody, true); err != nil { return err } } newAddress := &pmapi.MessageAddress{Type: sendingInfo.Scheme, Signature: signature} if sendingInfo.Encrypt && sendingInfo.PublicKey != nil { newAddress.BodyKeyPacket, newAddress.AttachmentKeyPackets, err = createPackets(sendingInfo.PublicKey, plainKey, attkeys) if err != nil { return err } } plainAddressMap[email] = newAddress plainSharedScheme |= sendingInfo.Scheme case pmapi.ContentTypeHTML: if htmlKey == nil { if htmlKey, htmlData, err = encryptSymmetric(kr, clearBody, true); err != nil { return err } } newAddress := &pmapi.MessageAddress{Type: sendingInfo.Scheme, Signature: signature} if sendingInfo.Encrypt && sendingInfo.PublicKey != nil { newAddress.BodyKeyPacket, newAddress.AttachmentKeyPackets, err = createPackets(sendingInfo.PublicKey, htmlKey, attkeys) if err != nil { return err } } htmlAddressMap[email] = newAddress htmlSharedScheme |= sendingInfo.Scheme } } } if containsUnencryptedRecipients { dec := new(mime.WordDecoder) subject, err := dec.DecodeHeader(message.Header.Get("Subject")) if err != nil { return errors.New("error decoding subject message " + message.Header.Get("Subject")) } if !su.continueSendingUnencryptedMail(subject) { _ = su.client.DeleteMessages([]string{message.ID}) return errors.New("sending was canceled by user") } } req := &pmapi.SendMessageReq{} plainPkg := buildPackage(plainAddressMap, plainSharedScheme, pmapi.ContentTypePlainText, plainData, plainKey, attkeysEncoded) if plainPkg != nil { req.Packages = append(req.Packages, plainPkg) } htmlPkg := buildPackage(htmlAddressMap, htmlSharedScheme, pmapi.ContentTypeHTML, htmlData, htmlKey, attkeysEncoded) if htmlPkg != nil { req.Packages = append(req.Packages, htmlPkg) } if len(mimeAddressMap) > 0 { pkg := &pmapi.MessagePackage{ Body: base64.StdEncoding.EncodeToString(mimeData), Addresses: mimeAddressMap, MIMEType: pmapi.ContentTypeMultipartMixed, Type: mimeSharedType, BodyKey: pmapi.AlgoKey{ Key: mimeKey.GetBase64Key(), Algorithm: mimeKey.Algo, }, } req.Packages = append(req.Packages, pkg) } return su.storeUser.SendMessage(message.ID, req) } func (su *smtpUser) handleReferencesHeader(m *pmapi.Message) (draftID, parentID string) { // Remove the internal IDs from the references header before sending to avoid confusion. references := m.Header.Get("References") newReferences := []string{} for _, reference := range strings.Fields(references) { if !strings.Contains(reference, "@protonmail.internalid") { newReferences = append(newReferences, reference) } else { // internalid is the parentID. idMatch := regexp.MustCompile(`(?U)<.*@protonmail.internalid>`).FindString(reference) if idMatch != "" { lastID := idMatch[1 : len(idMatch)-len("@protonmail.internalid>")] filter := &pmapi.MessagesFilter{ID: []string{lastID}} if su.addressID != "" { filter.AddressID = su.addressID } metadata, _, _ := su.client.ListMessages(filter) for _, m := range metadata { if m.IsDraft() { draftID = m.ID } else { parentID = m.ID } } } } } m.Header["References"] = newReferences if parentID == "" && len(newReferences) > 0 { externalID := strings.Trim(newReferences[len(newReferences)-1], "<>") filter := &pmapi.MessagesFilter{ExternalID: externalID} if su.addressID != "" { filter.AddressID = su.addressID } metadata, _, _ := su.client.ListMessages(filter) // There can be two or messages with the same external ID and then we cannot // be sure which message should be parent. Better to not choose any. if len(metadata) == 1 { parentID = metadata[0].ID } } return draftID, parentID } func (su *smtpUser) handleSenderAndRecipients(m *pmapi.Message, addr *pmapi.Address, from string, to []string) (err error) { from = pmapi.ConstructAddress(from, addr.Email) // Check sender. if m.Sender == nil { m.Sender = &mail.Address{Address: from} } else { m.Sender.Address = from } // Check recipients. if len(to) == 0 { err = errors.New("backend: no recipient specified") return } // Sanitize ToList because some clients add *Sender* in the *ToList* when only Bcc is filled. i := 0 for _, keep := range m.ToList { keepThis := false for _, addr := range to { if addr == keep.Address { keepThis = true break } } if keepThis { m.ToList[i] = keep i++ } } m.ToList = m.ToList[:i] // Build a map of recipients visible to all. // Bcc should be empty when sending a message. var recipients []*mail.Address recipients = append(recipients, m.ToList...) recipients = append(recipients, m.CCList...) recipients = append(recipients, m.BCCList...) rm := map[string]bool{} for _, r := range recipients { rm[r.Address] = true } for _, r := range to { if !rm[r] { // Recipient is not known, add it to Bcc. m.BCCList = append(m.BCCList, &mail.Address{Address: r}) } } return nil } func (su *smtpUser) continueSendingUnencryptedMail(subject string) bool { if !su.backend.shouldReportOutgoingNoEnc() { return true } messageID := strconv.Itoa(rand.Int()) //nolint[gosec] ch := make(chan bool) su.backend.shouldSendNoEncChannels[messageID] = ch su.eventListener.Emit(events.OutgoingNoEncEvent, messageID+":"+subject) log.Debug("Waiting for sendingUnencrypted confirmation for ", messageID) var res bool select { case res = <-ch: // GUI should always respond in 10 seconds, but let's have safety timeout // in case GUI will not respond properly. If GUI didn't respond, we cannot // be sure if user even saw the notice: better to not send the e-mail. log.Debug("Got sendingUnencrypted for ", messageID, ": ", res) case <-time.After(15 * time.Second): log.Debug("sendingUnencrypted timeout, not sending ", messageID) res = false } delete(su.backend.shouldSendNoEncChannels, messageID) close(ch) return res } // Logout is called when this User will no longer be used. func (su *smtpUser) Logout() error { log.Debug("SMTP client logged out user ", su.addressID) return nil }