forked from Silverfish/proton-bridge
194 lines
4.6 KiB
Go
194 lines
4.6 KiB
Go
package bridge
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"net"
|
|
"os"
|
|
"strconv"
|
|
|
|
"github.com/ProtonMail/proton-bridge/v2/internal/logging"
|
|
|
|
"github.com/Masterminds/semver/v3"
|
|
"github.com/ProtonMail/gluon"
|
|
imapEvents "github.com/ProtonMail/gluon/events"
|
|
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
|
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
defaultClientName = "UnknownClient"
|
|
defaultClientVersion = "0.0.1"
|
|
)
|
|
|
|
func (bridge *Bridge) serveIMAP() error {
|
|
imapListener, err := newListener(bridge.vault.GetIMAPPort(), bridge.vault.GetIMAPSSL(), bridge.tlsConfig)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create IMAP listener: %w", err)
|
|
}
|
|
|
|
bridge.imapListener = imapListener
|
|
|
|
if err := bridge.imapServer.Serve(context.Background(), bridge.imapListener); err != nil {
|
|
return fmt.Errorf("failed to serve IMAP: %w", err)
|
|
}
|
|
|
|
_, port, err := net.SplitHostPort(imapListener.Addr().String())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get IMAP listener address: %w", err)
|
|
}
|
|
|
|
portInt, err := strconv.Atoi(port)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to convert IMAP listener port to int: %w", err)
|
|
}
|
|
|
|
if portInt != bridge.vault.GetIMAPPort() {
|
|
if err := bridge.vault.SetIMAPPort(portInt); err != nil {
|
|
return fmt.Errorf("failed to update IMAP port in vault: %w", err)
|
|
}
|
|
}
|
|
|
|
go func() {
|
|
for err := range bridge.imapServer.GetErrorCh() {
|
|
logrus.WithError(err).Error("IMAP server error")
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (bridge *Bridge) restartIMAP() error {
|
|
if err := bridge.imapListener.Close(); err != nil {
|
|
logrus.WithError(err).Warn("Failed to close IMAP listener")
|
|
}
|
|
|
|
return bridge.serveIMAP()
|
|
}
|
|
|
|
func (bridge *Bridge) closeIMAP(ctx context.Context) error {
|
|
if err := bridge.imapServer.Close(ctx); err != nil {
|
|
return fmt.Errorf("failed to close IMAP server: %w", err)
|
|
}
|
|
|
|
if err := bridge.imapListener.Close(); err != nil {
|
|
return fmt.Errorf("failed to close IMAP listener: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
|
|
switch event := event.(type) {
|
|
case imapEvents.SessionAdded:
|
|
if bridge.identifier.HasClient() {
|
|
return
|
|
}
|
|
|
|
bridge.identifier.SetClient(defaultClientName, defaultClientVersion)
|
|
|
|
case imapEvents.IMAPID:
|
|
bridge.identifier.SetClient(event.IMAPID.Name, event.IMAPID.Version)
|
|
}
|
|
}
|
|
|
|
func getGluonDir(encVault *vault.Vault) (string, error) {
|
|
empty, exists, err := isEmpty(encVault.GetGluonDir())
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to check if gluon dir is empty: %w", err)
|
|
}
|
|
|
|
// TODO: Handle case that the gluon directory is missing and we can't create it!
|
|
if !exists {
|
|
if err := os.MkdirAll(encVault.GetGluonDir(), 0700); err != nil {
|
|
return "", fmt.Errorf("failed to create gluon dir: %w", err)
|
|
}
|
|
}
|
|
|
|
if empty {
|
|
if err := encVault.ForUser(func(user *vault.User) error {
|
|
return user.ClearSyncStatus()
|
|
}); err != nil {
|
|
return "", fmt.Errorf("failed to reset user sync status: %w", err)
|
|
}
|
|
}
|
|
|
|
return encVault.GetGluonDir(), nil
|
|
}
|
|
|
|
func newIMAPServer(
|
|
gluonDir string,
|
|
version *semver.Version,
|
|
tlsConfig *tls.Config,
|
|
logClient, logServer bool,
|
|
) (*gluon.Server, error) {
|
|
if logClient || logServer {
|
|
log := logrus.WithField("protocol", "IMAP")
|
|
log.Warning("================================================")
|
|
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
|
|
log.Warning("================================================")
|
|
}
|
|
|
|
var imapClientLog io.Writer
|
|
|
|
if logClient {
|
|
imapClientLog = logging.NewIMAPLogger()
|
|
} else {
|
|
imapClientLog = io.Discard
|
|
}
|
|
|
|
var imapServerLog io.Writer
|
|
|
|
if logServer {
|
|
imapServerLog = logging.NewIMAPLogger()
|
|
} else {
|
|
imapServerLog = io.Discard
|
|
}
|
|
|
|
imapServer, err := gluon.New(
|
|
gluon.WithTLS(tlsConfig),
|
|
gluon.WithDataDir(gluonDir),
|
|
gluon.WithVersionInfo(
|
|
int(version.Major()),
|
|
int(version.Minor()),
|
|
int(version.Patch()),
|
|
constants.FullAppName,
|
|
"TODO",
|
|
"TODO",
|
|
),
|
|
gluon.WithLogger(
|
|
imapClientLog,
|
|
imapServerLog,
|
|
),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return imapServer, nil
|
|
}
|
|
|
|
// isEmpty returns whether the given directory is empty.
|
|
// If the directory does not exist, the second return value is false.
|
|
func isEmpty(dir string) (bool, bool, error) {
|
|
if _, err := os.Stat(dir); err != nil {
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
return false, false, fmt.Errorf("failed to stat %s: %w", dir, err)
|
|
}
|
|
|
|
return true, false, nil
|
|
}
|
|
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return false, false, fmt.Errorf("failed to read dir %s: %w", dir, err)
|
|
}
|
|
|
|
return len(entries) == 0, true, nil
|
|
}
|