Other: Move sending logic to smtp.go
This commit is contained in:
2
go.mod
2
go.mod
@ -40,7 +40,7 @@ require (
|
|||||||
github.com/stretchr/testify v1.8.0
|
github.com/stretchr/testify v1.8.0
|
||||||
github.com/urfave/cli/v2 v2.20.3
|
github.com/urfave/cli/v2 v2.20.3
|
||||||
github.com/vmihailenco/msgpack/v5 v5.3.5
|
github.com/vmihailenco/msgpack/v5 v5.3.5
|
||||||
gitlab.protontech.ch/go/liteapi v0.41.3-0.20221111021557-10de395a8f9f
|
gitlab.protontech.ch/go/liteapi v0.42.1
|
||||||
go.uber.org/goleak v1.2.0
|
go.uber.org/goleak v1.2.0
|
||||||
golang.org/x/exp v0.0.0-20221023144134-a1e5550cf13e
|
golang.org/x/exp v0.0.0-20221023144134-a1e5550cf13e
|
||||||
golang.org/x/net v0.1.0
|
golang.org/x/net v0.1.0
|
||||||
|
|||||||
4
go.sum
4
go.sum
@ -403,8 +403,8 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsr
|
|||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/zclconf/go-cty v1.11.0 h1:726SxLdi2SDnjY+BStqB9J1hNp4+2WlzyXLuimibIe0=
|
github.com/zclconf/go-cty v1.11.0 h1:726SxLdi2SDnjY+BStqB9J1hNp4+2WlzyXLuimibIe0=
|
||||||
github.com/zclconf/go-cty v1.11.0/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA=
|
github.com/zclconf/go-cty v1.11.0/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA=
|
||||||
gitlab.protontech.ch/go/liteapi v0.41.3-0.20221111021557-10de395a8f9f h1:Vk8CdHAQTxYWhmvLHWbQSpTLW0Dj9SxqWdSWUr4fInA=
|
gitlab.protontech.ch/go/liteapi v0.42.1 h1:iKq/ZANPkFYiIr52ThVx7Jsn9YZw2CmwZyOlWtGKMVo=
|
||||||
gitlab.protontech.ch/go/liteapi v0.41.3-0.20221111021557-10de395a8f9f/go.mod h1:IM7ADWjgIL2hXopzx0WNamizEuMgM2QZl7QH12FNflk=
|
gitlab.protontech.ch/go/liteapi v0.42.1/go.mod h1:IM7ADWjgIL2hXopzx0WNamizEuMgM2QZl7QH12FNflk=
|
||||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
|
|||||||
@ -18,25 +18,144 @@
|
|||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/rfc822"
|
"github.com/ProtonMail/gluon/rfc822"
|
||||||
"github.com/ProtonMail/go-rfc5322"
|
"github.com/ProtonMail/go-rfc5322"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v2/internal/logging"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v2/internal/safe"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/pkg/message"
|
"github.com/ProtonMail/proton-bridge/v2/pkg/message"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/pkg/message/parser"
|
"github.com/ProtonMail/proton-bridge/v2/pkg/message/parser"
|
||||||
"github.com/bradenaw/juniper/parallel"
|
"github.com/bradenaw/juniper/parallel"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"gitlab.protontech.ch/go/liteapi"
|
"gitlab.protontech.ch/go/liteapi"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// sendMail sends an email from the given address to the given recipients.
|
||||||
|
//
|
||||||
|
// nolint:funlen
|
||||||
|
func (user *User) sendMail(authID string, from string, to []string, r io.Reader) error {
|
||||||
|
return safe.RLockRet(func() error {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if _, err := getAddrID(user.apiAddrs, from); err != nil {
|
||||||
|
return ErrInvalidReturnPath
|
||||||
|
}
|
||||||
|
|
||||||
|
emails := xslices.Map(maps.Values(user.apiAddrs), func(addr liteapi.Address) string {
|
||||||
|
return addr.Email
|
||||||
|
})
|
||||||
|
|
||||||
|
// Read the message to send.
|
||||||
|
b, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the hash of the message (to match it against SMTP messages).
|
||||||
|
hash, err := getMessageHash(b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we already tried to send this message recently.
|
||||||
|
if ok, err := user.sendHash.tryInsertWait(ctx, hash, to, time.Now().Add(90*time.Second)); err != nil {
|
||||||
|
return fmt.Errorf("failed to check send hash: %w", err)
|
||||||
|
} else if !ok {
|
||||||
|
user.log.Warn("A duplicate message was already sent recently, skipping")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we fail to send this message, we should remove the hash from the send recorder.
|
||||||
|
defer user.sendHash.removeOnFail(hash)
|
||||||
|
|
||||||
|
// Create a new message parser from the reader.
|
||||||
|
parser, err := parser.New(bytes.NewReader(b))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create parser: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the message contains a sender, use it instead of the one from the return path.
|
||||||
|
if sender, ok := getMessageSender(parser); ok {
|
||||||
|
from = sender
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the user's mail settings.
|
||||||
|
settings, err := user.client.GetMailSettings(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get mail settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
addrID, err := getAddrID(user.apiAddrs, from)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return withAddrKR(user.apiUser, user.apiAddrs[addrID], user.vault.KeyPass(), func(userKR, addrKR *crypto.KeyRing) error {
|
||||||
|
// Use the first key for encrypting the message.
|
||||||
|
addrKR, err := addrKR.FirstKey()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get first key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have to attach the public key, do it now.
|
||||||
|
if settings.AttachPublicKey == liteapi.AttachPublicKeyEnabled {
|
||||||
|
key, err := addrKR.GetKey(0)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get sending key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKey, err := key.GetArmoredPublicKey()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get public key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parser.AttachPublicKey(pubKey, fmt.Sprintf("publickey - %v - %v", addrKR.GetIdentities()[0].Name, key.GetFingerprint()[:8]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the message we want to send (after we have attached the public key).
|
||||||
|
message, err := message.ParseWithParser(parser)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the message using the correct key.
|
||||||
|
sent, err := sendWithKey(
|
||||||
|
ctx,
|
||||||
|
user.client,
|
||||||
|
authID,
|
||||||
|
user.vault.AddressMode(),
|
||||||
|
settings,
|
||||||
|
userKR, addrKR,
|
||||||
|
emails, from, to,
|
||||||
|
message,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the message was successfully sent, we can update the message ID in the record.
|
||||||
|
user.sendHash.addMessageID(hash, sent.ID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}, user.apiUserLock, user.apiAddrsLock)
|
||||||
|
}
|
||||||
|
|
||||||
// sendWithKey sends the message with the given address key.
|
// sendWithKey sends the message with the given address key.
|
||||||
func sendWithKey( //nolint:funlen
|
func sendWithKey( //nolint:funlen
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
@ -259,15 +378,12 @@ func createAttachments(
|
|||||||
}
|
}
|
||||||
|
|
||||||
keys, err := parallel.MapContext(ctx, runtime.NumCPU(), attachments, func(ctx context.Context, att message.Attachment) (attKey, error) {
|
keys, err := parallel.MapContext(ctx, runtime.NumCPU(), attachments, func(ctx context.Context, att message.Attachment) (attKey, error) {
|
||||||
sig, err := addrKR.SignDetached(crypto.NewPlainMessage(att.Data))
|
logrus.WithFields(logrus.Fields{
|
||||||
if err != nil {
|
"name": logging.Sensitive(att.Name),
|
||||||
return attKey{}, fmt.Errorf("failed to sign attachment: %w", err)
|
"contentID": att.ContentID,
|
||||||
}
|
"disposition": att.Disposition,
|
||||||
|
"mime-type": att.MIMEType,
|
||||||
encData, err := addrKR.EncryptAttachment(crypto.NewPlainMessage(att.Data), att.Name)
|
}).Debug("Uploading attachment")
|
||||||
if err != nil {
|
|
||||||
return attKey{}, fmt.Errorf("failed to encrypt attachment: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some clients use inline disposition but don't set a content ID. Our API doesn't support this.
|
// Some clients use inline disposition but don't set a content ID. Our API doesn't support this.
|
||||||
// We could generate our own content ID, but for simplicity, we just set the disposition to attachment.
|
// We could generate our own content ID, but for simplicity, we just set the disposition to attachment.
|
||||||
@ -275,15 +391,13 @@ func createAttachments(
|
|||||||
att.Disposition = string(liteapi.AttachmentDisposition)
|
att.Disposition = string(liteapi.AttachmentDisposition)
|
||||||
}
|
}
|
||||||
|
|
||||||
attachment, err := client.UploadAttachment(ctx, liteapi.CreateAttachmentReq{
|
attachment, err := client.UploadAttachment(ctx, addrKR, liteapi.CreateAttachmentReq{
|
||||||
Filename: att.Name,
|
Filename: att.Name,
|
||||||
MessageID: draftID,
|
MessageID: draftID,
|
||||||
MIMEType: rfc822.MIMEType(att.MIMEType),
|
MIMEType: rfc822.MIMEType(att.MIMEType),
|
||||||
Disposition: liteapi.Disposition(att.Disposition),
|
Disposition: liteapi.Disposition(att.Disposition),
|
||||||
ContentID: att.ContentID,
|
ContentID: att.ContentID,
|
||||||
KeyPackets: encData.KeyPacket,
|
Body: att.Data,
|
||||||
DataPacket: encData.DataPacket,
|
|
||||||
Signature: sig.GetBinary(),
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return attKey{}, fmt.Errorf("failed to upload attachment: %w", err)
|
return attKey{}, fmt.Errorf("failed to upload attachment: %w", err)
|
||||||
|
|||||||
@ -18,7 +18,6 @@
|
|||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -31,14 +30,11 @@ import (
|
|||||||
"github.com/ProtonMail/gluon/imap"
|
"github.com/ProtonMail/gluon/imap"
|
||||||
"github.com/ProtonMail/gluon/queue"
|
"github.com/ProtonMail/gluon/queue"
|
||||||
gluonReporter "github.com/ProtonMail/gluon/reporter"
|
gluonReporter "github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/async"
|
"github.com/ProtonMail/proton-bridge/v2/internal/async"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
"github.com/ProtonMail/proton-bridge/v2/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/logging"
|
"github.com/ProtonMail/proton-bridge/v2/internal/logging"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v2/internal/safe"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/pkg/message"
|
|
||||||
"github.com/ProtonMail/proton-bridge/v2/pkg/message/parser"
|
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -382,116 +378,11 @@ func (user *User) NewIMAPConnectors() (map[string]connector.Connector, error) {
|
|||||||
func (user *User) SendMail(authID string, from string, to []string, r io.Reader) error {
|
func (user *User) SendMail(authID string, from string, to []string, r io.Reader) error {
|
||||||
defer user.goPoll()
|
defer user.goPoll()
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if len(to) == 0 {
|
if len(to) == 0 {
|
||||||
return ErrInvalidRecipient
|
return ErrInvalidRecipient
|
||||||
}
|
}
|
||||||
|
|
||||||
return safe.RLockRet(func() error {
|
return user.sendMail(authID, from, to, r)
|
||||||
if _, err := getAddrID(user.apiAddrs, from); err != nil {
|
|
||||||
return ErrInvalidReturnPath
|
|
||||||
}
|
|
||||||
|
|
||||||
emails := xslices.Map(maps.Values(user.apiAddrs), func(addr liteapi.Address) string {
|
|
||||||
return addr.Email
|
|
||||||
})
|
|
||||||
|
|
||||||
// Read the message to send.
|
|
||||||
b, err := io.ReadAll(r)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read message: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute the hash of the message (to match it against SMTP messages).
|
|
||||||
hash, err := getMessageHash(b)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we already tried to send this message recently.
|
|
||||||
if ok, err := user.sendHash.tryInsertWait(ctx, hash, to, time.Now().Add(90*time.Second)); err != nil {
|
|
||||||
return fmt.Errorf("failed to check send hash: %w", err)
|
|
||||||
} else if !ok {
|
|
||||||
user.log.Warn("A duplicate message was already sent recently, skipping")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we fail to send this message, we should remove the hash from the send recorder.
|
|
||||||
defer user.sendHash.removeOnFail(hash)
|
|
||||||
|
|
||||||
// Create a new message parser from the reader.
|
|
||||||
parser, err := parser.New(bytes.NewReader(b))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create parser: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the message contains a sender, use it instead of the one from the return path.
|
|
||||||
if sender, ok := getMessageSender(parser); ok {
|
|
||||||
from = sender
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the user's mail settings.
|
|
||||||
settings, err := user.client.GetMailSettings(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get mail settings: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
addrID, err := getAddrID(user.apiAddrs, from)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return withAddrKR(user.apiUser, user.apiAddrs[addrID], user.vault.KeyPass(), func(userKR, addrKR *crypto.KeyRing) error {
|
|
||||||
// Use the first key for encrypting the message.
|
|
||||||
addrKR, err := addrKR.FirstKey()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get first key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have to attach the public key, do it now.
|
|
||||||
if settings.AttachPublicKey == liteapi.AttachPublicKeyEnabled {
|
|
||||||
key, err := addrKR.GetKey(0)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get sending key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
pubKey, err := key.GetArmoredPublicKey()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get public key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
parser.AttachPublicKey(pubKey, fmt.Sprintf("publickey - %v - %v", addrKR.GetIdentities()[0].Name, key.GetFingerprint()[:8]))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the message we want to send (after we have attached the public key).
|
|
||||||
message, err := message.ParseWithParser(parser)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to parse message: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the message using the correct key.
|
|
||||||
sent, err := sendWithKey(
|
|
||||||
ctx,
|
|
||||||
user.client,
|
|
||||||
authID,
|
|
||||||
user.vault.AddressMode(),
|
|
||||||
settings,
|
|
||||||
userKR, addrKR,
|
|
||||||
emails, from, to,
|
|
||||||
message,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to send message: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the message was successfully sent, we can update the message ID in the record.
|
|
||||||
user.sendHash.addMessageID(hash, sent.ID)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}, user.apiUserLock, user.apiAddrsLock)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckAuth returns whether the given email and password can be used to authenticate over IMAP or SMTP with this user.
|
// CheckAuth returns whether the given email and password can be used to authenticate over IMAP or SMTP with this user.
|
||||||
|
|||||||
Reference in New Issue
Block a user