diff --git a/internal/configstatus/config_status.go b/internal/configstatus/config_status.go
new file mode 100644
index 00000000..d73ea198
--- /dev/null
+++ b/internal/configstatus/config_status.go
@@ -0,0 +1,146 @@
+// Copyright (c) 2023 Proton AG
+//
+// This file is part of Proton Mail Bridge.
+//
+// Proton Mail 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.
+//
+// Proton Mail 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 Proton Mail Bridge. If not, see .
+
+package configstatus
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "time"
+
+ "github.com/ProtonMail/proton-bridge/v3/internal/safe"
+ "github.com/sirupsen/logrus"
+)
+
+const version = "1.0.0"
+
+func LoadConfigurationStatus(filepath string) (*ConfigurationStatus, error) {
+ status := ConfigurationStatus{
+ FilePath: filepath,
+ DataLock: safe.NewRWMutex(),
+ Data: &ConfigurationStatusData{},
+ }
+
+ if _, err := os.Stat(filepath); err == nil {
+ if err := status.Load(); err == nil {
+ return &status, nil
+ }
+ logrus.WithError(err).Warn("Cannot load configuration status file. Reset it.")
+ }
+
+ status.Data.init()
+ if err := status.Save(); err != nil {
+ return &status, err
+ }
+ return &status, nil
+}
+
+func (status *ConfigurationStatus) Load() error {
+ bytes, err := os.ReadFile(status.FilePath)
+ if err != nil {
+ return err
+ }
+
+ var metadata MetadataOnly
+ if err := json.Unmarshal(bytes, &metadata); err != nil {
+ return err
+ }
+
+ if metadata.Metadata.Version != version {
+ return fmt.Errorf("unsupported configstatus file version %s", metadata.Metadata.Version)
+ }
+
+ return json.Unmarshal(bytes, status.Data)
+}
+
+func (status *ConfigurationStatus) Save() error {
+ temp := status.FilePath + "_temp"
+ f, err := os.Create(temp) //nolint:gosec
+ if err != nil {
+ return err
+ }
+
+ err = json.NewEncoder(f).Encode(status.Data)
+ if err := f.Close(); err != nil {
+ logrus.WithError(err).Error("Error while closing configstatus file.")
+ }
+ if err != nil {
+ return err
+ }
+
+ return os.Rename(temp, status.FilePath)
+}
+
+func (status *ConfigurationStatus) Success() error {
+ status.DataLock.Lock()
+ defer status.DataLock.Unlock()
+
+ status.Data.init()
+ status.Data.DataV1.PendingSince = time.Time{}
+ return status.Save()
+}
+
+func (status *ConfigurationStatus) Failure(err string) error {
+ status.DataLock.Lock()
+ defer status.DataLock.Unlock()
+
+ status.Data.init()
+ status.Data.DataV1.FailureDetails = err
+ return status.Save()
+}
+
+func (status *ConfigurationStatus) Progress() error {
+ status.DataLock.Lock()
+ defer status.DataLock.Unlock()
+
+ status.Data.DataV1.LastProgress = time.Now()
+ return status.Save()
+}
+
+func (status *ConfigurationStatus) RecordLinkClicked(link uint) error {
+ status.DataLock.Lock()
+ defer status.DataLock.Unlock()
+
+ if !status.Data.hasLinkClicked(link) {
+ status.Data.setClickedLink(link)
+ return status.Save()
+ }
+ return nil
+}
+
+func (data *ConfigurationStatusData) init() {
+ data.Metadata = Metadata{
+ Version: version,
+ }
+ data.DataV1.PendingSince = time.Now()
+ data.DataV1.LastProgress = time.Time{}
+ data.DataV1.Autoconf = ""
+ data.DataV1.ClickedLink = 0
+ data.DataV1.ReportSent = false
+ data.DataV1.ReportClick = false
+ data.DataV1.FailureDetails = ""
+}
+
+func (data *ConfigurationStatusData) setClickedLink(pos uint) {
+ data.DataV1.ClickedLink |= 1 << pos
+}
+
+func (data *ConfigurationStatusData) hasLinkClicked(pos uint) bool {
+ val := data.DataV1.ClickedLink & (1 << pos)
+ return val > 0
+}
diff --git a/internal/configstatus/types_config_status.go b/internal/configstatus/types_config_status.go
new file mode 100644
index 00000000..032c63ca
--- /dev/null
+++ b/internal/configstatus/types_config_status.go
@@ -0,0 +1,54 @@
+// Copyright (c) 2023 Proton AG
+//
+// This file is part of Proton Mail Bridge.
+//
+// Proton Mail 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.
+//
+// Proton Mail 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 Proton Mail Bridge. If not, see .
+
+package configstatus
+
+import (
+ "time"
+
+ "github.com/ProtonMail/proton-bridge/v3/internal/safe"
+)
+
+type Metadata struct {
+ Version string `json:"version"`
+}
+
+type MetadataOnly struct {
+ Metadata Metadata `json:"metadata"`
+}
+
+type DataV1 struct {
+ PendingSince time.Time `json:"auto_update"`
+ LastProgress time.Time `json:"last_progress"`
+ Autoconf string `json:"auto_conf"`
+ ClickedLink uint64 `json:"clicked_link"`
+ ReportSent bool `json:"report_sent"`
+ ReportClick bool `json:"report_click"`
+ FailureDetails string `json:"failure_details"`
+}
+
+type ConfigurationStatusData struct {
+ Metadata Metadata `json:"metadata"`
+ DataV1 DataV1 `json:"dataV1"`
+}
+
+type ConfigurationStatus struct {
+ FilePath string
+ DataLock safe.RWMutex
+
+ Data *ConfigurationStatusData
+}
diff --git a/internal/telemetry/configuration_abort.go b/internal/telemetry/configuration_abort.go
index 4e6566cf..a79a7d37 100644
--- a/internal/telemetry/configuration_abort.go
+++ b/internal/telemetry/configuration_abort.go
@@ -16,3 +16,5 @@
// along with Proton Mail Bridge. If not, see .
package telemetry
+
+// GODT-2711
diff --git a/internal/telemetry/configuration_progress.go b/internal/telemetry/configuration_progress.go
index 4e6566cf..42963f5a 100644
--- a/internal/telemetry/configuration_progress.go
+++ b/internal/telemetry/configuration_progress.go
@@ -16,3 +16,5 @@
// along with Proton Mail Bridge. If not, see .
package telemetry
+
+// GODT-2713
diff --git a/internal/telemetry/configuration_recovery.go b/internal/telemetry/configuration_recovery.go
index 4e6566cf..7c1a5055 100644
--- a/internal/telemetry/configuration_recovery.go
+++ b/internal/telemetry/configuration_recovery.go
@@ -16,3 +16,5 @@
// along with Proton Mail Bridge. If not, see .
package telemetry
+
+// GODT-2714
diff --git a/internal/telemetry/configuration_success.go b/internal/telemetry/configuration_success.go
index 4e6566cf..134e2909 100644
--- a/internal/telemetry/configuration_success.go
+++ b/internal/telemetry/configuration_success.go
@@ -16,3 +16,5 @@
// along with Proton Mail Bridge. If not, see .
package telemetry
+
+// GODT-2710
diff --git a/internal/user/config_status.go b/internal/user/config_status.go
deleted file mode 100644
index c18d4e34..00000000
--- a/internal/user/config_status.go
+++ /dev/null
@@ -1,127 +0,0 @@
-// Copyright (c) 2023 Proton AG
-//
-// This file is part of Proton Mail Bridge.
-//
-// Proton Mail 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.
-//
-// Proton Mail 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 Proton Mail Bridge. If not, see .
-
-package user
-
-import (
- "encoding/json"
- "os"
- "time"
-)
-
-type ConfigurationStatusData struct {
- PendingSince time.Time `json:"auto_update"`
- LastProgress time.Time `json:"last_progress"`
- Autoconf string `json:"auto_conf"`
- ClickedLink uint64 `json:"clicked_link"`
- ReportSent bool `json:"report_sent"`
- ReportClick bool `json:"report_click"`
- FailureDetails string `json:"failure_details"`
-}
-
-type ConfigurationStatus struct {
- FilePath string
- Data ConfigurationStatusData
-}
-
-func LoadConfigurationStatus(filepath string) (*ConfigurationStatus, error) {
- status := ConfigurationStatus{
- FilePath: filepath,
- Data: ConfigurationStatusData{},
- }
- if _, err := os.Stat(filepath); err == nil {
- if err := status.Data.load(filepath); err == nil {
- return &status, nil
- }
- } else {
- status.Data.init()
- if err := status.save(); err == nil {
- return &status, nil
- }
- }
- return &status, nil
-}
-
-func (status *ConfigurationStatus) Success() error {
- status.Data.init()
- status.Data.PendingSince = time.Time{}
- return status.save()
-}
-
-func (status *ConfigurationStatus) Failure(err string) error {
- status.Data.init()
- status.Data.FailureDetails = err
- return status.save()
-}
-
-func (status *ConfigurationStatus) Progress() error {
- status.Data.LastProgress = time.Now()
- return status.save()
-}
-
-func (status *ConfigurationStatus) RecordLinkClicked(link uint) error {
- if !status.Data.hasLinkClicked(link) {
- status.Data.setClickedLink(link)
- return status.save()
- }
- return nil
-}
-
-func (status *ConfigurationStatus) save() error {
- return status.Data.save(status.FilePath)
-}
-
-func (data *ConfigurationStatusData) init() {
- data.PendingSince = time.Now()
- data.LastProgress = time.Time{}
- data.Autoconf = ""
- data.ClickedLink = 0
- data.ReportSent = false
- data.ReportClick = false
- data.FailureDetails = ""
-}
-
-func (data *ConfigurationStatusData) load(filepath string) error {
- f, err := os.Open(filepath) // nolint: gosec
- if err != nil {
- return err
- }
-
- defer func() { _ = f.Close() }()
-
- return json.NewDecoder(f).Decode(data)
-}
-
-func (data *ConfigurationStatusData) save(filepath string) error {
- f, err := os.Create(filepath) // nolint: gosec
- if err != nil {
- return err
- }
-
- defer func() { _ = f.Close() }()
-
- return json.NewEncoder(f).Encode(data)
-}
-
-func (data *ConfigurationStatusData) setClickedLink(pos uint) {
- data.ClickedLink |= 1 << pos
-}
-
-func (data *ConfigurationStatusData) hasLinkClicked(pos uint) bool {
- val := data.ClickedLink & (1 << pos)
- return val > 0
-}
diff --git a/internal/user/user.go b/internal/user/user.go
index 69b14d58..d9e898b2 100644
--- a/internal/user/user.go
+++ b/internal/user/user.go
@@ -36,6 +36,7 @@ import (
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal"
+ "github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
@@ -94,7 +95,7 @@ type User struct {
panicHandler async.PanicHandler
- configStatus *ConfigurationStatus
+ configStatus *configstatus.ConfigurationStatus
}
// New returns a new user.
@@ -130,7 +131,7 @@ func New(
}).Info("Creating user object")
configStatusFile := filepath.Join(cacheDir, apiUser.ID+".json")
- configStatus, err := LoadConfigurationStatus(configStatusFile)
+ configStatus, err := configstatus.LoadConfigurationStatus(configStatusFile)
if err != nil {
return nil, fmt.Errorf("failed to init configuration status file: %w", err)
}