GODT-1554 / 1555: Implement gRPC go service and Qt 5 frontend C++ app.

WIP: updates

WIP: cache on disk and autostart.

WIP: mail, keychain and more.

WIP: updated grpc version in go mod file.

WIP: user list.

WIP: RPC service placeholder

WIP: test C++ RPC client skeleton.

Other: missing license script update.

WIP: use Qt test framework.

WIP: test for app and login calls.

WIP: test for update & cache on disk calls.

WIP: tests for mail settings calls.

WIP: all client tests.

WIP: linter fixes.

WIP: fix missing license link.

WIP: update dependency_license script for gRPC and protobuf.

WIP: removed unused file.

WIP: app & login event streaming tests.

WIP: update event stream tests.

WIP: completed event streaming tests.

GODT-1554: qt C++ frontend skeleton.

WIP: C++ backend declaration.

wip: started drafting user model.

WIP: users. not functional.

WIP: invokable methods

WIP: Exception class + backend 'injection' into QML.

WIP: switch to VCPKG to ease multi-arch compilation,  C++ RPC client skeleton.

WIP: Renaming and reorganisation

WIP:introduced new 'grpc' go frontend.

WIP: Worker & Oveerseer for thread management.

WIP: added log to C++ app.

WIP: event stream architecture on Go side.

WIP: event parsing and streamer stopping.

WIP: Moved grpc to frontend subfolder + use vcpkg for gRPC and protobuf.

WIP: windows building ok

WIP: wired a few messages

WIP: more wiring.

WIP: Fixed imports after rebase on top of devel.

WIP: wired some bool and string properties.

WIP: more properties.

WIP: wired cache on disk stuff

WIP: connect event watcher.

WIP: login

WIP: fix showSplashScreen

WIP: Wired login calls.

WIP: user list.

WIP: Refactored main().

WIP: User retrieval .

WIP: no shared pointer in user model.

WIP: fixed user count.

WIP: cached goos.

WIP: Wired autostart

WIP: beta channel toggle wired.

WIP: User removal

WIP: wired theme

WIP: implemented configure apple mail.

WIP: split mode.

WIP: fixed user updates.

WIP: fixed Quit from tray icon

WIP: wired CurrentEmailClient

WIP: wired UseSSLForSMTP

WIP: wired change ports .

WIP: wired DoH. .

WIP: wired keychain calls.

WIP: wired autoupdate option.

WIP: QML Backend clean-up.

WIP: cleanup.

WIP: moved user related files in subfolder. .

WIP: User are managed using smart pointers.

WIP: cleanup.

WIP: more cleanup.

WIP: mail events forwarding

WIP: code inspection tweaks from CLion.

WIP: moved QML, cleanup, and missing copyright notices.

WIP: Backend is not QMLBackend.

Other: fixed issues reported by Leander. [skip ci]
This commit is contained in:
Xavier Michelon
2022-05-16 10:59:45 +02:00
committed by Jakub
parent a4e54f063d
commit c11fe3e1ab
183 changed files with 53334 additions and 10851 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,424 @@
// 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/>.
syntax = "proto3";
import "google/protobuf/empty.proto";
import "google/protobuf/wrappers.proto";
option go_package = "github.com/ProtonMail/proton-bridge/internal/grpc";
package grpc; // ignored by Go, used as namespace name in C++.
//**********************************************************************************************************************
// Service Declaration
//**********************************************************************************************************************
service Bridge {
// App related calls
rpc GuiReady (google.protobuf.Empty) returns (google.protobuf.Empty);
rpc Quit (google.protobuf.Empty) returns (google.protobuf.Empty);
rpc Restart (google.protobuf.Empty) returns (google.protobuf.Empty);
rpc ShowOnStartup(google.protobuf.Empty) returns (google.protobuf.BoolValue);
rpc ShowSplashScreen(google.protobuf.Empty) returns (google.protobuf.BoolValue);
rpc IsFirstGuiStart(google.protobuf.Empty) returns (google.protobuf.BoolValue);
rpc SetIsAutostartOn(google.protobuf.BoolValue) returns (google.protobuf.Empty);
rpc IsAutostartOn(google.protobuf.Empty) returns (google.protobuf.BoolValue);
rpc SetIsBetaEnabled(google.protobuf.BoolValue) returns (google.protobuf.Empty);
rpc IsBetaEnabled(google.protobuf.Empty) returns (google.protobuf.BoolValue);
rpc GoOs(google.protobuf.Empty) returns (google.protobuf.StringValue);
rpc TriggerReset(google.protobuf.Empty) returns (google.protobuf.Empty);
rpc Version(google.protobuf.Empty) returns (google.protobuf.StringValue);
rpc LogsPath(google.protobuf.Empty) returns (google.protobuf.StringValue);
rpc LicensePath(google.protobuf.Empty) returns (google.protobuf.StringValue);
// rpc ReleaseNotesLink(google.protobuf.Empty) returns (google.protobuf.StringValue); // TODO GODT-1670 Apparently cannot be polled for now, will be sent as update.
rpc DependencyLicensesLink(google.protobuf.Empty) returns (google.protobuf.StringValue);
// rpc LandingPageLink(google.protobuf.Empty) returns (google.protobuf.StringValue); // TODO GODT-1670 Apparently cannot be polled for now, will be sent as update.
rpc SetColorSchemeName(google.protobuf.StringValue) returns (google.protobuf.Empty);
rpc ColorSchemeName(google.protobuf.Empty) returns (google.protobuf.StringValue); // TODO Color scheme should probably entirely be managed by the client.
rpc CurrentEmailClient(google.protobuf.Empty) returns (google.protobuf.StringValue);
rpc ReportBug(ReportBugRequest) returns (google.protobuf.Empty);
// login
rpc Login(LoginRequest) returns (google.protobuf.Empty);
rpc Login2FA(LoginRequest) returns (google.protobuf.Empty);
rpc Login2Passwords(LoginRequest) returns (google.protobuf.Empty);
rpc LoginAbort(LoginAbortRequest) returns (google.protobuf.Empty);
// update
rpc CheckUpdate(google.protobuf.Empty) returns (google.protobuf.Empty);
rpc InstallUpdate(google.protobuf.Empty) returns (google.protobuf.Empty);
rpc SetIsAutomaticUpdateOn(google.protobuf.BoolValue) returns (google.protobuf.Empty);
rpc IsAutomaticUpdateOn(google.protobuf.Empty) returns (google.protobuf.BoolValue);
// cache
rpc IsCacheOnDiskEnabled (google.protobuf.Empty) returns (google.protobuf.BoolValue);
rpc DiskCachePath(google.protobuf.Empty) returns (google.protobuf.StringValue);
rpc ChangeLocalCache(ChangeLocalCacheRequest) returns (google.protobuf.Empty);
// mail
rpc SetIsDoHEnabled(google.protobuf.BoolValue) returns (google.protobuf.Empty);
rpc IsDoHEnabled(google.protobuf.Empty) returns (google.protobuf.BoolValue);
rpc SetUseSslForSmtp(google.protobuf.BoolValue) returns (google.protobuf.Empty);
rpc UseSslForSmtp(google.protobuf.Empty) returns (google.protobuf.BoolValue);
rpc Hostname(google.protobuf.Empty) returns (google.protobuf.StringValue);
rpc ImapPort(google.protobuf.Empty) returns (google.protobuf.Int32Value);
rpc SmtpPort(google.protobuf.Empty) returns (google.protobuf.Int32Value);
rpc ChangePorts(ChangePortsRequest) returns (google.protobuf.Empty);
rpc IsPortFree(google.protobuf.Int32Value) returns (google.protobuf.BoolValue);
// keychain
rpc AvailableKeychains(google.protobuf.Empty) returns (AvailableKeychainsResponse);
rpc SetCurrentKeychain(google.protobuf.StringValue) returns (google.protobuf.Empty);
rpc CurrentKeychain(google.protobuf.Empty) returns (google.protobuf.StringValue);
// User & user list
rpc GetUserList(google.protobuf.Empty) returns (UserListResponse);
rpc GetUser(google.protobuf.StringValue) returns (User);
rpc SetUserSplitMode(UserSplitModeRequest) returns (google.protobuf.Empty);
rpc LogoutUser(google.protobuf.StringValue) returns (google.protobuf.Empty);
rpc RemoveUser(google.protobuf.StringValue) returns (google.protobuf.Empty);
rpc ConfigureUserAppleMail(ConfigureAppleMailRequest) returns (google.protobuf.Empty);
// Server -> Client event stream
rpc StartEventStream(google.protobuf.Empty) returns (stream StreamEvent); // Keep streaming until StopEventStream is called.
rpc StopEventStream(google.protobuf.Empty) returns (google.protobuf.Empty);
}
//**********************************************************************************************************************
// RPC calls requests and replies messages
//**********************************************************************************************************************
message ReportBugRequest {
string description = 1;
string address = 2;
string emailClient = 3;
bool includeLogs = 4;
}
// login related messages
message LoginRequest {
string username = 1;
string password = 2;
}
message LoginAbortRequest {
string username = 1;
}
//**********************************************************
// Cache on disk related messages
//**********************************************************
message ChangeLocalCacheRequest {
bool enableDiskCache = 1;
string diskCachePath = 2;
}
//**********************************************************
// Cache on disk related messages
//**********************************************************
message ChangePortsRequest {
int32 imapPort = 1;
int32 smtpPort = 2;
}
//**********************************************************
// Cache on disk related messages
//**********************************************************
message AvailableKeychainsResponse {
repeated string keychains = 1;
}
//**********************************************************
// Cache on disk related messages
//**********************************************************
message User {
string id = 1;
string username = 2;
string avatarText = 3;
bool loggedIn = 4;
bool splitMode = 5;
bool setupGuideSeen = 6;
int64 usedBytes = 7;
int64 totalBytes = 8;
string password = 9;
repeated string addresses = 10;
}
message UserSplitModeRequest {
string userID = 1;
bool active = 2;
}
message UserListResponse {
repeated User users = 1;
}
message ConfigureAppleMailRequest {
string userID = 1;
string address = 2;
}
//**********************************************************************************************************************
// Event stream messages
//**********************************************************************************************************************
message StreamEvent {
oneof event {
AppEvent app = 1;
LoginEvent login = 2;
UpdateEvent update = 3;
CacheEvent cache = 4;
MailSettingsEvent mailSettings = 5;
KeychainEvent keychain = 6;
MailEvent mail = 7;
UserEvent user = 8;
}
}
//**********************************************************
// App related events
//**********************************************************
message AppEvent {
oneof event {
InternetStatusEvent internetStatus = 1;
ToggleAutostartFinishedEvent toggleAutostartFinished = 2;
ResetFinishedEvent resetFinished = 3;
ReportBugFinishedEvent reportBugFinished = 4;
ReportBugSuccessEvent reportBugSuccess = 5;
ReportBugErrorEvent reportBugError = 6;
ShowMainWindowEvent showMainWindow = 7;
}
}
message InternetStatusEvent {
bool connected = 1;
}
message ToggleAutostartFinishedEvent {}
message ResetFinishedEvent {}
message ReportBugFinishedEvent {}
message ReportBugSuccessEvent {}
message ReportBugErrorEvent {}
message ShowMainWindowEvent {}
//**********************************************************
// Login related events
//**********************************************************
message LoginEvent {
oneof event {
LoginErrorEvent error = 1;
LoginTfaRequestedEvent tfaRequested = 2;
LoginTwoPasswordsRequestedEvent twoPasswordRequested = 3;
LoginFinishedEvent finished = 4;
LoginFinishedEvent alreadyLoggedIn = 5;
}
}
enum LoginErrorType {
USERNAME_PASSWORD_ERROR = 0;
FREE_USER = 1;
CONNECTION_ERROR = 2;
TFA_ERROR = 3;
TFA_ABORT = 4;
TWO_PASSWORDS_ERROR = 5;
TWO_PASSWORDS_ABORT = 6;
}
message LoginErrorEvent {
LoginErrorType type = 1;
string message = 2;
}
message LoginTfaRequestedEvent {
string username = 1;
}
message LoginTwoPasswordsRequestedEvent {}
message LoginFinishedEvent {
string userID = 1;
}
//**********************************************************
// Update related events
//**********************************************************
message UpdateEvent {
oneof event {
UpdateErrorEvent error = 1;
UpdateManualReadyEvent manualReady = 2;
UpdateManualRestartNeededEvent manualRestartNeeded = 3;
UpdateForceEvent force = 4;
UpdateSilentRestartNeeded silentRestartNeeded = 5;
UpdateIsLatestVersion isLatestVersion = 6;
UpdateCheckFinished checkFinished = 7;
}
}
enum UpdateErrorType {
UPDATE_MANUAL_ERROR = 0;
UPDATE_FORCE_ERROR = 1;
UPDATE_SILENT_ERROR = 2;
}
message UpdateErrorEvent {
UpdateErrorType type = 1;
}
message UpdateManualReadyEvent {
string version = 1;
}
message UpdateManualRestartNeededEvent {};
message UpdateForceEvent {
string version = 1;
}
message UpdateSilentRestartNeeded {}
message UpdateIsLatestVersion {}
message UpdateCheckFinished {}
//**********************************************************
// Cache on disk related events
//**********************************************************
message CacheEvent {
oneof event {
CacheErrorEvent error = 1;
CacheLocationChangeSuccessEvent locationChangedSuccess = 2;
ChangeLocalCacheFinishedEvent changeLocalCacheFinished = 3;
IsCacheOnDiskEnabledChanged isCacheOnDiskEnabledChanged = 4;
DiskCachePathChanged diskCachePathChanged = 5;
}
}
enum CacheErrorType {
CACHE_UNAVAILABLE_ERROR = 0;
CACHE_CANT_MOVE_ERROR = 1;
DISK_FULL = 2;
};
message CacheErrorEvent {
CacheErrorType type = 1;
}
message CacheLocationChangeSuccessEvent {};
message ChangeLocalCacheFinishedEvent {};
message IsCacheOnDiskEnabledChanged {
bool enabled = 1;
}
message DiskCachePathChanged {
string path = 1;
}
//**********************************************************
// Mail settings related events
//**********************************************************
message MailSettingsEvent {
oneof event {
MailSettingsErrorEvent error = 1;
UseSslForSmtpFinishedEvent useSslForSmtpFinished = 2;
ChangePortsFinishedEvent changePortsFinished = 3;
}
}
enum MailSettingsErrorType {
IMAP_PORT_ISSUE = 0;
SMTP_PORT_ISSUE = 1;
}
message MailSettingsErrorEvent {
MailSettingsErrorType type = 1;
}
message UseSslForSmtpFinishedEvent {}
message ChangePortsFinishedEvent {}
//**********************************************************
// keychain related events
//**********************************************************
message KeychainEvent {
oneof event {
ChangeKeychainFinishedEvent changeKeychainFinished = 1;
HasNoKeychainEvent hasNoKeychain = 2;
RebuildKeychainEvent rebuildKeychain = 3;
}
}
message ChangeKeychainFinishedEvent {}
message HasNoKeychainEvent {}
message RebuildKeychainEvent {}
//**********************************************************
// Mail related events
//**********************************************************
message MailEvent {
oneof event {
NoActiveKeyForRecipientEvent noActiveKeyForRecipientEvent = 1;
AddressChangedEvent addressChanged = 2;
AddressChangedLogoutEvent addressChangedLogout = 3;
ApiCertIssueEvent apiCertIssue = 6;
}
}
message NoActiveKeyForRecipientEvent {
string email = 1;
}
message AddressChangedEvent {
string address = 1;
}
message AddressChangedLogoutEvent {
string address = 1;
}
message ApiCertIssueEvent {}
//**********************************************************
// User list related event
//**********************************************************
message UserEvent {
oneof event {
ToggleSplitModeFinishedEvent toggleSplitModeFinished= 1;
UserDisconnectedEvent userDisconnected = 2;
UserChangedEvent userChanged = 3;
}
}
message ToggleSplitModeFinishedEvent {
string userID = 1;
}
message UserDisconnectedEvent {
string username = 1;
}
message UserChangedEvent {
string userID = 1;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,69 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.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 <https://www.gnu.org/licenses/>.
package grpc
//goland:noinspection SpellCheckingInspection
const (
serverCert = `-----BEGIN CERTIFICATE-----
MIIC5TCCAc2gAwIBAgIJAMUQK0VGexMsMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV
BAMMCWxvY2FsaG9zdDAeFw0yMjA2MTQxNjUyNTVaFw0yMjA3MTQxNjUyNTVaMBQx
EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
ggEBAL6T1JQ0jptq512PBLASpCLFB0px7KIzEml0oMUCkVgUF+2cayrvdBXJZnaO
SG+/JPnHDcQ/ecgqkh2Ii6a2x2kWA5KqWiV+bSHp0drXyUGJfM85muLsnrhYwJ83
HHtweoUVebRZvHn66KjaH8nBJ+YVWyYbSUhJezcg6nBSEtkW+I/XUHu4S2C7FUc5
DXPO3yWWZuZ22OZz70DY3uYE/9COuilotuKdj7XgeKDyKIvRXjPFyqGxwnnp6bXC
vWvrQdcxy0wM+vZxew3QtA/Ag9uKJU9owP6noauXw95l49lEVIA5KXVNtdaldVht
MO/QoelLZC7h79PK22zbii3x930CAwEAAaM6MDgwFAYDVR0RBA0wC4IJbG9jYWxo
b3N0MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B
AQsFAAOCAQEAW/9PE8dcAN+0C3K96Xd6Y3qOOtQhRw+WlZXhtiqMtlJfTjvuGKs9
58xuKcTvU5oobxLv+i5+4gpqLjUZZ9FBnYXZIACNVzq4PEXf+YdzcA+y6RS/rqT4
dUjsuYrScAmdXK03Duw3HWYrTp8gsJzIaYGTltUrOn0E4k/TsZb/tZ6z+oH7Fi+p
wdsI6Ut6Zwm3Z7WLn5DDk8KvFjHjZkdsCb82SFSAUVrzWo5EtbLIY/7y3A5rGp9D
t0AVpuGPo5Vn+MW1WA9HT8lhjz0v5wKGMOBi3VYW+Yx8FWHDpacvbZwVM0MjMSAd
M7SXYbNDiLF4LwPLsunoLsW133Ky7s99MA==
-----END CERTIFICATE-----`
serverKey = `-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+k9SUNI6baudd
jwSwEqQixQdKceyiMxJpdKDFApFYFBftnGsq73QVyWZ2jkhvvyT5xw3EP3nIKpId
iIumtsdpFgOSqlolfm0h6dHa18lBiXzPOZri7J64WMCfNxx7cHqFFXm0Wbx5+uio
2h/JwSfmFVsmG0lISXs3IOpwUhLZFviP11B7uEtguxVHOQ1zzt8llmbmdtjmc+9A
2N7mBP/QjropaLbinY+14Hig8iiL0V4zxcqhscJ56em1wr1r60HXMctMDPr2cXsN
0LQPwIPbiiVPaMD+p6Grl8PeZePZRFSAOSl1TbXWpXVYbTDv0KHpS2Qu4e/Tytts
24ot8fd9AgMBAAECggEBAJFkGpOOnRU4s5YO3BavwgS8p9lFnLAJooxNa7GhSd0W
R0MBSEkTMU7FvaPI3L5T5xOfpoMHohLxV1Osrk3bt7oWD1e/GtLr5routejtIx8a
kttNKTriJhyhqSJOWy5ZGz+YqKbMpxuwLftTnVjAQX4o4MbrnjbFyHjAZdqW4sY2
jLulfEdOave6nxaEocmIkoXEjuX90LB+yNG6ncSYM3GV+IyCVw7DsoU4dLd/IRDa
4iJVF7tVdAsZqN6/EVYXpGqG0t1HI8ddacHa1qWgCG3kBB+3faxXZcDJdlRrXLUQ
4jLH8oEfXOb5YgCwyYzW2EynXEpG5vjsPmsCWJY/mIECgYEA52av81+lui97KLg+
T07XtR8zJPMkHnBNfc6ooWku/+0NuQPpUq14vqzRVut9jBHUDP3xSvrPnXsp15ZA
/mipLQLNKssTYtk90cyGqLUkrd/NPLFZLXToBfWBlfazdcJQQRIxZ2dTy5MH+HIU
Oio3LZi+iDIbdzzSlmL8PaLit20CgYEA0tYsswhq6OaWx25iu4hBMRlt6hr9qGVW
jlzCFjBhlh3YtoBti2w2fsJdU+hUpeXU327fhFmdCQFXtf+Om5CSHihmJ+mHj9O1
5Jd6zn4o8szdg5je9T4gt7KG6QdXaFJ2aMuq+SxZl1NIE+9qnf/qom4GHHZ/Nj41
vwlQu+zS5lECgYAOzSK0DoorPp5CHIbfy8tAap563pKQ394VDgL7UB8Rf7hA/V8P
SslOaP9679U4AGvv6M5mXWSqThZ/E71UiJ1Jo8Q72IGE8SBjKxHx+KQ/+vDF0RJD
NhchSnLfhMg14BgCEYfXdWSGwQDhg2qHzet5nyuQyqO3HMzbkblQt/qIgQKBgHLv
nPiQmy+SHRplO9+93MQ2d6wKwMNfUztSp9/OyjQ62xxKkO1TtbWOobAPVK4Hx+9y
EtmkvK3fFIC763M08eMM5PvXHDa1FFCkn6cYMZyDQDLwUINjNhTOdytr/CN76N8i
QHeLzN9o4D814mp1y+R2lFBJ7PmWGlilbGS2KxaxAoGAFMsb1MER+eTOUO3z05Di
lts4VRWQhq2frd/on6AcTv4idQox1RcOrKWQbRVgeQVY1SkkHhg8lN0jX3W3EfuQ
aOfyky04GbLiwO8NRHZMlORWLxlCkrUrb6Va+LQlT0JvpQbqdbu6Ix8NomG9K697
aScKmY7bGC0ki2IIdt2YZ5I=
-----END PRIVATE KEY-----`
)

View File

@ -0,0 +1,199 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.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 <https://www.gnu.org/licenses/>.
package grpc
func NewInternetStatusEvent(connected bool) *StreamEvent {
return appEvent(&AppEvent{Event: &AppEvent_InternetStatus{InternetStatus: &InternetStatusEvent{Connected: connected}}})
}
func NewToggleAutostartFinishedEvent() *StreamEvent {
return appEvent(&AppEvent{Event: &AppEvent_ToggleAutostartFinished{ToggleAutostartFinished: &ToggleAutostartFinishedEvent{}}})
}
func NewResetFinishedEvent() *StreamEvent {
return appEvent(&AppEvent{Event: &AppEvent_ResetFinished{ResetFinished: &ResetFinishedEvent{}}})
}
func NewReportBugFinishedEvent() *StreamEvent {
return appEvent(&AppEvent{Event: &AppEvent_ReportBugFinished{ReportBugFinished: &ReportBugFinishedEvent{}}})
}
func NewReportBugSuccessEvent() *StreamEvent {
return appEvent(&AppEvent{Event: &AppEvent_ReportBugSuccess{ReportBugSuccess: &ReportBugSuccessEvent{}}})
}
func NewReportBugErrorEvent() *StreamEvent {
return appEvent(&AppEvent{Event: &AppEvent_ReportBugError{ReportBugError: &ReportBugErrorEvent{}}})
}
func NewShowMainWindowEvent() *StreamEvent {
return appEvent(&AppEvent{Event: &AppEvent_ShowMainWindow{ShowMainWindow: &ShowMainWindowEvent{}}})
}
func NewLoginError(err LoginErrorType, message string) *StreamEvent {
return loginEvent(&LoginEvent{Event: &LoginEvent_Error{Error: &LoginErrorEvent{Type: err, Message: message}}})
}
func NewLoginTfaRequestedEvent(username string) *StreamEvent {
return loginEvent(&LoginEvent{Event: &LoginEvent_TfaRequested{TfaRequested: &LoginTfaRequestedEvent{Username: username}}})
}
func NewLoginTwoPasswordsRequestedEvent() *StreamEvent {
return loginEvent(&LoginEvent{Event: &LoginEvent_TwoPasswordRequested{}})
}
func NewLoginFinishedEvent(userID string) *StreamEvent {
return loginEvent(&LoginEvent{Event: &LoginEvent_Finished{Finished: &LoginFinishedEvent{UserID: userID}}})
}
func NewLoginAlreadyLoggedInEvent(userID string) *StreamEvent {
return loginEvent(&LoginEvent{Event: &LoginEvent_AlreadyLoggedIn{AlreadyLoggedIn: &LoginFinishedEvent{UserID: userID}}})
}
func NewUpdateErrorEvent(errorType UpdateErrorType) *StreamEvent {
return updateEvent(&UpdateEvent{Event: &UpdateEvent_Error{Error: &UpdateErrorEvent{Type: errorType}}})
}
func NewUpdateManualReadyEvent(version string) *StreamEvent {
return updateEvent(&UpdateEvent{Event: &UpdateEvent_ManualReady{ManualReady: &UpdateManualReadyEvent{Version: version}}})
}
func NewUpdateManualRestartNeededEvent() *StreamEvent {
return updateEvent(&UpdateEvent{Event: &UpdateEvent_ManualRestartNeeded{ManualRestartNeeded: &UpdateManualRestartNeededEvent{}}})
}
func NewUpdateForceEvent(version string) *StreamEvent {
return updateEvent(&UpdateEvent{Event: &UpdateEvent_Force{Force: &UpdateForceEvent{Version: version}}})
}
func NewUpdateSilentRestartNeededEvent() *StreamEvent {
return updateEvent(&UpdateEvent{Event: &UpdateEvent_SilentRestartNeeded{SilentRestartNeeded: &UpdateSilentRestartNeeded{}}})
}
func NewUpdateIsLatestVersionEvent() *StreamEvent {
return updateEvent(&UpdateEvent{Event: &UpdateEvent_IsLatestVersion{IsLatestVersion: &UpdateIsLatestVersion{}}})
}
func NewUpdateCheckFinishedEvent() *StreamEvent {
return updateEvent(&UpdateEvent{Event: &UpdateEvent_CheckFinished{CheckFinished: &UpdateCheckFinished{}}})
}
func NewCacheErrorEvent(err CacheErrorType) *StreamEvent {
return cacheEvent(&CacheEvent{Event: &CacheEvent_Error{Error: &CacheErrorEvent{Type: err}}})
}
func NewCacheLocationChangeSuccessEvent() *StreamEvent {
return cacheEvent(&CacheEvent{Event: &CacheEvent_LocationChangedSuccess{LocationChangedSuccess: &CacheLocationChangeSuccessEvent{}}})
}
func NewCacheChangeLocalCacheFinishedEvent() *StreamEvent {
return cacheEvent(&CacheEvent{Event: &CacheEvent_ChangeLocalCacheFinished{ChangeLocalCacheFinished: &ChangeLocalCacheFinishedEvent{}}})
}
func NewIsCacheOnDiskEnabledChanged(enabled bool) *StreamEvent {
return cacheEvent(&CacheEvent{Event: &CacheEvent_IsCacheOnDiskEnabledChanged{IsCacheOnDiskEnabledChanged: &IsCacheOnDiskEnabledChanged{Enabled: enabled}}})
}
func NewDiskCachePathChanged(path string) *StreamEvent {
return cacheEvent(&CacheEvent{Event: &CacheEvent_DiskCachePathChanged{DiskCachePathChanged: &DiskCachePathChanged{Path: path}}})
}
func NewMailSettingsErrorEvent(err MailSettingsErrorType) *StreamEvent {
return mailSettingsEvent(&MailSettingsEvent{Event: &MailSettingsEvent_Error{Error: &MailSettingsErrorEvent{Type: err}}})
}
func NewMailSettingsUseSslForSmtpFinishedEvent() *StreamEvent { //nolint:revive,stylecheck
return mailSettingsEvent(&MailSettingsEvent{Event: &MailSettingsEvent_UseSslForSmtpFinished{UseSslForSmtpFinished: &UseSslForSmtpFinishedEvent{}}})
}
func NewMailSettingsChangePortFinishedEvent() *StreamEvent {
return mailSettingsEvent(&MailSettingsEvent{Event: &MailSettingsEvent_ChangePortsFinished{ChangePortsFinished: &ChangePortsFinishedEvent{}}})
}
func NewKeychainChangeKeychainFinishedEvent() *StreamEvent {
return keychainEvent(&KeychainEvent{Event: &KeychainEvent_ChangeKeychainFinished{ChangeKeychainFinished: &ChangeKeychainFinishedEvent{}}})
}
func NewKeychainHasNoKeychainEvent() *StreamEvent {
return keychainEvent(&KeychainEvent{Event: &KeychainEvent_HasNoKeychain{HasNoKeychain: &HasNoKeychainEvent{}}})
}
func NewKeychainRebuildKeychainEvent() *StreamEvent {
return keychainEvent(&KeychainEvent{Event: &KeychainEvent_RebuildKeychain{RebuildKeychain: &RebuildKeychainEvent{}}})
}
func NewMailNoActiveKeyForRecipientEvent(email string) *StreamEvent {
return mailEvent(&MailEvent{Event: &MailEvent_NoActiveKeyForRecipientEvent{NoActiveKeyForRecipientEvent: &NoActiveKeyForRecipientEvent{Email: email}}})
}
func NewMailAddressChangeEvent(email string) *StreamEvent {
return mailEvent(&MailEvent{Event: &MailEvent_AddressChanged{AddressChanged: &AddressChangedEvent{Address: email}}})
}
func NewMailAddressChangeLogoutEvent(email string) *StreamEvent {
return mailEvent(&MailEvent{Event: &MailEvent_AddressChangedLogout{AddressChangedLogout: &AddressChangedLogoutEvent{Address: email}}})
}
func NewMailApiCertIssue() *StreamEvent { //nolint:revive,stylecheck
return mailEvent(&MailEvent{Event: &MailEvent_ApiCertIssue{ApiCertIssue: &ApiCertIssueEvent{}}})
}
func NewUserToggleSplitModeFinishedEvent(userID string) *StreamEvent {
return userEvent(&UserEvent{Event: &UserEvent_ToggleSplitModeFinished{ToggleSplitModeFinished: &ToggleSplitModeFinishedEvent{UserID: userID}}})
}
func NewUserDisconnectedEvent(email string) *StreamEvent {
return userEvent(&UserEvent{Event: &UserEvent_UserDisconnected{UserDisconnected: &UserDisconnectedEvent{Username: email}}})
}
func NewUserChangedEvent(userID string) *StreamEvent {
return userEvent(&UserEvent{Event: &UserEvent_UserChanged{UserChanged: &UserChangedEvent{UserID: userID}}})
}
// Event category factory functions.
func appEvent(appEvent *AppEvent) *StreamEvent {
return &StreamEvent{Event: &StreamEvent_App{App: appEvent}}
}
func loginEvent(event *LoginEvent) *StreamEvent {
return &StreamEvent{Event: &StreamEvent_Login{Login: event}}
}
func updateEvent(event *UpdateEvent) *StreamEvent {
return &StreamEvent{Event: &StreamEvent_Update{Update: event}}
}
func cacheEvent(event *CacheEvent) *StreamEvent {
return &StreamEvent{Event: &StreamEvent_Cache{Cache: event}}
}
func mailSettingsEvent(event *MailSettingsEvent) *StreamEvent {
return &StreamEvent{Event: &StreamEvent_MailSettings{MailSettings: event}}
}
func keychainEvent(event *KeychainEvent) *StreamEvent {
return &StreamEvent{Event: &StreamEvent_Keychain{Keychain: event}}
}
func mailEvent(event *MailEvent) *StreamEvent {
return &StreamEvent{Event: &StreamEvent_Mail{Mail: event}}
}
func userEvent(event *UserEvent) *StreamEvent {
return &StreamEvent{Event: &StreamEvent_User{User: event}}
}

View File

@ -0,0 +1,329 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.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 <https://www.gnu.org/licenses/>.
package grpc
//go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative bridge.proto
import (
"crypto/tls"
"net"
"runtime"
"strings"
"sync"
"time"
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
"github.com/ProtonMail/proton-bridge/v2/internal/config/useragent"
"github.com/ProtonMail/proton-bridge/v2/internal/events"
"github.com/ProtonMail/proton-bridge/v2/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/v2/internal/locations"
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
"github.com/ProtonMail/proton-bridge/v2/internal/users"
"github.com/ProtonMail/proton-bridge/v2/pkg/keychain"
"github.com/ProtonMail/proton-bridge/v2/pkg/listener"
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
"github.com/sirupsen/logrus"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
// Service is the RPC service struct.
type Service struct { // nolint:structcheck
UnimplementedBridgeServer
grpcServer *grpc.Server // the gGRPC server
listener net.Listener
eventStreamCh chan *StreamEvent
eventStreamDoneCh chan struct{}
programName string
programVersion string
panicHandler types.PanicHandler
locations *locations.Locations
settings *settings.Settings
eventListener listener.Listener
updater types.Updater
userAgent *useragent.UserAgent
bridge types.Bridger
restarter types.Restarter
showOnStartup bool
authClient pmapi.Client
auth *pmapi.Auth
password []byte
// newVersionInfo updater.VersionInfo // TO-DO GODT-1670 Implement version check
log *logrus.Entry
initializing sync.WaitGroup
initializationDone sync.Once
firstTimeAutostart sync.Once
}
// NewService returns a new instance of the service.
func NewService(
version,
programName string,
showOnStartup bool,
panicHandler types.PanicHandler,
locations *locations.Locations,
settings *settings.Settings,
eventListener listener.Listener,
updater types.Updater,
userAgent *useragent.UserAgent,
bridge types.Bridger,
_ types.NoEncConfirmator,
restarter types.Restarter,
) *Service {
s := Service{
UnimplementedBridgeServer: UnimplementedBridgeServer{},
programName: programName,
programVersion: version,
panicHandler: panicHandler,
locations: locations,
settings: settings,
eventListener: eventListener,
updater: updater,
userAgent: userAgent,
bridge: bridge,
restarter: restarter,
showOnStartup: showOnStartup,
log: logrus.WithField("pkg", "grpc"),
initializing: sync.WaitGroup{},
initializationDone: sync.Once{},
firstTimeAutostart: sync.Once{},
}
s.userAgent.SetPlatform(runtime.GOOS) // TO-DO GODT-1672 In the previous Qt frontend, this routine used QSysInfo::PrettyProductName to return a more accurate description, e.g. "Windows 10" or "MacOS 10.12"
cert, err := tls.X509KeyPair([]byte(serverCert), []byte(serverKey))
if err != nil {
s.log.WithError(err).Error("could not create key pair")
panic(err)
}
s.initAutostart()
s.grpcServer = grpc.NewServer(grpc.Creds(credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS13,
})))
RegisterBridgeServer(s.grpcServer, &s)
s.listener, err = net.Listen("tcp", "127.0.0.1:9292") // Port should be configurable from the command-line.
if err != nil {
s.log.WithError(err).Error("could not create listener")
panic(err)
}
return &s
}
func (s *Service) initAutostart() {
// GODT-1507 Windows: autostart needs to be created after Qt is initialized.
// GODT-1206: if preferences file says it should be on enable it here.
// TO-DO GODT-1681 Autostart needs to be properly implement for gRPC approach.
s.firstTimeAutostart.Do(func() {
shouldAutostartBeOn := s.settings.GetBool(settings.AutostartKey)
if s.bridge.IsFirstStart() || shouldAutostartBeOn {
if err := s.bridge.EnableAutostart(); err != nil {
s.log.WithField("prefs", shouldAutostartBeOn).WithError(err).Error("Failed to enable first autostart")
}
return
}
})
}
func (s *Service) Loop() error {
defer func() {
s.settings.SetBool(settings.FirstStartGUIKey, false)
}()
go func() {
defer s.panicHandler.HandlePanic()
s.watchEvents()
}()
err := s.grpcServer.Serve(s.listener)
if err != nil {
s.log.WithError(err).Error("error serving RPC")
return err
}
return nil
}
// frontend interface functions TODO GODT-1670 Implement
func (s *Service) NotifyManualUpdate( /* update */ _ updater.VersionInfo /*canInstall */, _ bool) {}
func (s *Service) SetVersion( /* update */ updater.VersionInfo) {}
func (s *Service) NotifySilentUpdateInstalled() {}
func (s *Service) NotifySilentUpdateError(error) {}
func (s *Service) WaitUntilFrontendIsReady() {}
func (s *Service) watchEvents() { // nolint:funlen
if s.bridge.HasError(bridge.ErrLocalCacheUnavailable) {
_ = s.SendEvent(NewCacheErrorEvent(CacheErrorType_CACHE_UNAVAILABLE_ERROR))
}
errorCh := s.eventListener.ProvideChannel(events.ErrorEvent)
credentialsErrorCh := s.eventListener.ProvideChannel(events.CredentialsErrorEvent)
noActiveKeyForRecipientCh := s.eventListener.ProvideChannel(events.NoActiveKeyForRecipientEvent)
internetConnChangedCh := s.eventListener.ProvideChannel(events.InternetConnChangedEvent)
secondInstanceCh := s.eventListener.ProvideChannel(events.SecondInstanceEvent)
restartBridgeCh := s.eventListener.ProvideChannel(events.RestartBridgeEvent)
addressChangedCh := s.eventListener.ProvideChannel(events.AddressChangedEvent)
addressChangedLogoutCh := s.eventListener.ProvideChannel(events.AddressChangedLogoutEvent)
logoutCh := s.eventListener.ProvideChannel(events.LogoutEvent)
updateApplicationCh := s.eventListener.ProvideChannel(events.UpgradeApplicationEvent)
userChangedCh := s.eventListener.ProvideChannel(events.UserRefreshEvent)
certIssue := s.eventListener.ProvideChannel(events.TLSCertIssue)
// we forward events to the GUI/frontend via the gRPC event stream.
for {
select {
case errorDetails := <-errorCh:
if strings.Contains(errorDetails, "IMAP failed") {
_ = s.SendEvent(NewMailSettingsErrorEvent(MailSettingsErrorType_IMAP_PORT_ISSUE))
}
if strings.Contains(errorDetails, "SMTP failed") {
_ = s.SendEvent(NewMailSettingsErrorEvent(MailSettingsErrorType_SMTP_PORT_ISSUE))
}
case reason := <-credentialsErrorCh:
if reason == keychain.ErrMacKeychainRebuild.Error() {
_ = s.SendEvent(NewKeychainRebuildKeychainEvent())
continue
}
_ = s.SendEvent(NewKeychainHasNoKeychainEvent())
case email := <-noActiveKeyForRecipientCh:
_ = s.SendEvent(NewMailNoActiveKeyForRecipientEvent(email))
case stat := <-internetConnChangedCh:
if stat == events.InternetOff {
_ = s.SendEvent(NewInternetStatusEvent(false))
}
if stat == events.InternetOn {
_ = s.SendEvent(NewInternetStatusEvent(true))
}
case <-secondInstanceCh:
_ = s.SendEvent(NewShowMainWindowEvent())
case <-restartBridgeCh:
s.restart()
case address := <-addressChangedCh:
_ = s.SendEvent(NewMailAddressChangeEvent(address))
case address := <-addressChangedLogoutCh:
_ = s.SendEvent(NewMailAddressChangeLogoutEvent(address))
case userID := <-logoutCh:
user, err := s.bridge.GetUser(userID)
if err != nil {
return
}
_ = s.SendEvent(NewUserDisconnectedEvent(user.Username()))
case <-updateApplicationCh:
s.updateForce()
case userID := <-userChangedCh:
_ = s.SendEvent(NewUserChangedEvent(userID))
case <-certIssue:
_ = s.SendEvent(NewMailApiCertIssue())
}
}
}
func (s *Service) loginAbort() {
s.loginClean()
}
func (s *Service) loginClean() {
s.auth = nil
s.authClient = nil
for i := range s.password {
s.password[i] = '\x00'
}
s.password = s.password[0:0]
}
func (s *Service) finishLogin() {
defer s.loginClean()
if len(s.password) == 0 || s.auth == nil || s.authClient == nil {
s.log.
WithField("hasPass", len(s.password) != 0).
WithField("hasAuth", s.auth != nil).
WithField("hasClient", s.authClient != nil).
Error("Finish login: authentication incomplete")
_ = s.SendEvent(NewLoginError(LoginErrorType_TWO_PASSWORDS_ABORT, "Missing authentication, try again."))
return
}
done := make(chan string)
s.eventListener.Add(events.UserChangeDone, done)
defer s.eventListener.Remove(events.UserChangeDone, done)
user, err := s.bridge.FinishLogin(s.authClient, s.auth, s.password)
if err != nil && err != users.ErrUserAlreadyConnected {
s.log.WithError(err).Errorf("Finish login failed")
_ = s.SendEvent(NewLoginError(LoginErrorType_TWO_PASSWORDS_ABORT, err.Error()))
return
}
// The user changed should be triggered by FinishLogin, but it is not
// guaranteed when this is going to happen. Therefor we should wait
// until we receive the signal from userChanged function.
s.waitForUserChangeDone(done, user.ID())
s.log.WithField("userID", user.ID()).Debug("Login finished")
_ = s.SendEvent(NewLoginFinishedEvent(user.ID()))
if err == users.ErrUserAlreadyConnected {
s.log.WithError(err).Error("User already logged in")
_ = s.SendEvent(NewLoginAlreadyLoggedInEvent(user.ID()))
}
}
func (s *Service) waitForUserChangeDone(done <-chan string, userID string) {
for {
select {
case changedID := <-done:
if changedID == userID {
return
}
case <-time.After(2 * time.Second):
s.log.WithField("ID", userID).Warning("Login finished but user not added within 2 seconds")
return
}
}
}
func (s *Service) restart() {
s.log.Error("Restart is not implemented") // TO-DO GODT-1671 implement restart.
}
func (s *Service) checkUpdate() {
s.log.Error("checkUpdate is not implemented") // TO-DO GODT-1670 implement update check.
}
func (s *Service) updateForce() {
s.log.Error("updateForce is not implemented") // TO-DO GODT-1670 implement update.
}
func (s *Service) checkUpdateAndNotify() {
s.log.Error("checkUpdateAndNotify is not implemented") // TO-DO GODT-1670 implement update check.
}

View File

@ -0,0 +1,543 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.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 <https://www.gnu.org/licenses/>.
package grpc
import (
"context"
"encoding/base64"
"runtime"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/pkg/ports"
"github.com/ProtonMail/proton-bridge/v2/internal/bridge"
"github.com/ProtonMail/proton-bridge/v2/internal/config/settings"
"github.com/ProtonMail/proton-bridge/v2/internal/frontend/theme"
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
"github.com/ProtonMail/proton-bridge/v2/pkg/keychain"
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
var ErrNotImplemented = status.Errorf(codes.Unimplemented, "Not implemented")
// GuiReady implement the GuiReady gRPC service call.
func (s *Service) GuiReady(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
s.log.Info("GuiReady")
// Note nothing to be done. old Qt frontend had a sync.one
return &emptypb.Empty{}, nil
}
// Quit implement the Quit gRPC service call.
func (s *Service) Quit(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
s.log.Info("Quit")
var err error
if s.eventStreamCh != nil {
if _, err = s.StopEventStream(ctx, empty); err != nil {
s.log.WithError(err).Error("Quit failed.")
}
}
// The following call is launched as a goroutine, as it will wait for current calls to end, including this one.
go func() { s.grpcServer.GracefulStop() }()
return &emptypb.Empty{}, err
}
// Restart implement the Restart gRPC service call.
func (s *Service) Restart(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
s.log.Info("Restart") // TO-DO-GODT-1671 handle restart.
s.restart()
return nil, ErrNotImplemented
}
func (s *Service) ShowOnStartup(context.Context, *emptypb.Empty) (*wrapperspb.BoolValue, error) {
s.log.Info("ShowOnStartup")
return wrapperspb.Bool(s.showOnStartup), nil
}
func (s *Service) ShowSplashScreen(context.Context, *emptypb.Empty) (*wrapperspb.BoolValue, error) {
s.log.Info("ShowSplashScreen")
if s.bridge.IsFirstStart() {
return wrapperspb.Bool(false), nil
}
ver, err := semver.NewVersion(s.bridge.GetLastVersion())
if err != nil {
s.log.WithError(err).WithField("last", s.bridge.GetLastVersion()).Debug("Cannot parse last version")
return wrapperspb.Bool(false), nil
}
// Current splash screen contains update on rebranding. Therefore, it
// should be shown only if the last used version was less than 2.2.0.
return wrapperspb.Bool(ver.LessThan(semver.MustParse("2.2.0"))), nil
}
func (s *Service) IsFirstGuiStart(context.Context, *emptypb.Empty) (*wrapperspb.BoolValue, error) {
s.log.Info("IsFirstGuiStart")
return wrapperspb.Bool(s.settings.GetBool(settings.FirstStartGUIKey)), nil
}
func (s *Service) SetIsAutostartOn(_ context.Context, isOn *wrapperspb.BoolValue) (*emptypb.Empty, error) {
s.log.WithField("show", isOn.Value).Info("SetIsAutostartOn")
defer func() { _ = s.SendEvent(NewToggleAutostartFinishedEvent()) }()
if isOn.Value == s.bridge.IsAutostartEnabled() {
s.initAutostart()
return &emptypb.Empty{}, nil
}
var err error
if isOn.Value {
err = s.bridge.EnableAutostart()
} else {
err = s.bridge.DisableAutostart()
}
s.initAutostart()
if err != nil {
s.log.WithField("makeItEnabled", isOn.Value).WithError(err).Error("Autostart change failed")
}
return &emptypb.Empty{}, nil
}
func (s *Service) IsAutostartOn(context.Context, *emptypb.Empty) (*wrapperspb.BoolValue, error) {
s.log.Info("IsAutostartOn")
return wrapperspb.Bool(s.bridge.IsAutostartEnabled()), nil
}
func (s *Service) SetIsBetaEnabled(_ context.Context, isEnabled *wrapperspb.BoolValue) (*emptypb.Empty, error) {
s.log.WithField("isEnabled", isEnabled.Value).Info("SetIsBetaEnabled")
channel := updater.StableChannel
if isEnabled.Value {
channel = updater.EarlyChannel
}
s.bridge.SetUpdateChannel(channel)
s.checkUpdate()
return &emptypb.Empty{}, nil
}
func (s *Service) IsBetaEnabled(context.Context, *emptypb.Empty) (*wrapperspb.BoolValue, error) {
s.log.Info("IsBetaEnabled")
return wrapperspb.Bool(s.bridge.GetUpdateChannel() == updater.EarlyChannel), nil
}
func (s *Service) GoOs(context.Context, *emptypb.Empty) (*wrapperspb.StringValue, error) {
s.log.Info("GoOs") // TO-DO We can probably get rid of this and use QSysInfo::product name
return wrapperspb.String(runtime.GOOS), nil
}
func (s *Service) TriggerReset(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
s.log.Info("TriggerReset")
return nil, ErrNotImplemented
}
func (s *Service) Version(context.Context, *emptypb.Empty) (*wrapperspb.StringValue, error) {
s.log.Info("Version")
return nil, ErrNotImplemented
}
func (s *Service) LogsPath(context.Context, *emptypb.Empty) (*wrapperspb.StringValue, error) {
s.log.Info("LogsPath")
path, err := s.locations.ProvideLogsPath()
if err != nil {
s.log.WithError(err).Error("Cannot determine logs path")
return nil, err
}
return wrapperspb.String(path), nil
}
func (s *Service) LicensePath(context.Context, *emptypb.Empty) (*wrapperspb.StringValue, error) {
s.log.Info("LicensePath")
return wrapperspb.String(s.locations.GetLicenseFilePath()), nil
}
func (s *Service) DependencyLicensesLink(context.Context, *emptypb.Empty) (*wrapperspb.StringValue, error) {
return wrapperspb.String(s.locations.GetDependencyLicensesLink()), nil
}
func (s *Service) SetColorSchemeName(_ context.Context, name *wrapperspb.StringValue) (*emptypb.Empty, error) {
s.log.WithField("ColorSchemeName", name.Value).Info("SetColorSchemeName")
if !theme.IsAvailable(theme.Theme(name.Value)) {
s.log.WithField("scheme", name.Value).Warn("Color scheme not available")
return nil, status.Error(codes.NotFound, "Color scheme not available")
}
s.settings.Set(settings.ColorScheme, name.Value)
return &emptypb.Empty{}, nil
}
func (s *Service) ColorSchemeName(context.Context, *emptypb.Empty) (*wrapperspb.StringValue, error) {
s.log.Info("ColorSchemeName")
current := s.settings.Get(settings.ColorScheme)
if !theme.IsAvailable(theme.Theme(current)) {
current = string(theme.DefaultTheme())
s.settings.Set(settings.ColorScheme, current)
}
return wrapperspb.String(current), nil
}
func (s *Service) CurrentEmailClient(context.Context, *emptypb.Empty) (*wrapperspb.StringValue, error) {
s.log.Info("CurrentEmailClient")
return wrapperspb.String(s.userAgent.String()), nil
}
func (s *Service) ReportBug(_ context.Context, report *ReportBugRequest) (*emptypb.Empty, error) {
s.log.WithField("description", report.Description).
WithField("address", report.Address).
WithField("emailClient", report.EmailClient).
WithField("includeLogs", report.IncludeLogs).
Info("ReportBug")
return &emptypb.Empty{}, nil
}
func (s *Service) Login(_ context.Context, login *LoginRequest) (*emptypb.Empty, error) {
s.log.WithField("username", login.Username).Info("Login")
go func() {
defer s.panicHandler.HandlePanic()
var err error
s.password, err = base64.StdEncoding.DecodeString(login.Password)
if err != nil {
s.log.WithError(err).Error("Cannot decode password")
_ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, "Cannot decode password"))
s.loginClean()
return
}
s.authClient, s.auth, err = s.bridge.Login(login.Username, s.password)
if err != nil {
if err == pmapi.ErrPasswordWrong {
// Remove error message since it is hardcoded in QML.
_ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, ""))
s.loginClean()
return
}
if err == pmapi.ErrPaidPlanRequired {
_ = s.SendEvent(NewLoginError(LoginErrorType_FREE_USER, ""))
s.loginClean()
return
}
_ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, err.Error()))
s.loginClean()
return
}
if s.auth.HasTwoFactor() {
_ = s.SendEvent(NewLoginTfaRequestedEvent(login.Username))
return
}
if s.auth.HasMailboxPassword() {
_ = s.SendEvent(NewLoginTwoPasswordsRequestedEvent())
return
}
s.finishLogin()
}()
return &emptypb.Empty{}, nil
}
func (s *Service) Login2FA(_ context.Context, login *LoginRequest) (*emptypb.Empty, error) {
s.log.WithField("username", login.Username).Info("Login2FA")
go func() {
defer s.panicHandler.HandlePanic()
if s.auth == nil || s.authClient == nil {
s.log.Errorf("Login 2FA: authethication incomplete %p %p", s.auth, s.authClient)
_ = s.SendEvent(NewLoginError(LoginErrorType_TFA_ABORT, "Missing authentication, try again."))
s.loginClean()
return
}
twoFA, err := base64.StdEncoding.DecodeString(login.Password)
if err != nil {
s.log.WithError(err).Error("Cannot decode 2fa code")
_ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, "Cannot decode 2fa code"))
s.loginClean()
return
}
err = s.authClient.Auth2FA(context.Background(), string(twoFA))
if err == pmapi.ErrBad2FACodeTryAgain {
s.log.Warn("Login 2FA: retry 2fa")
_ = s.SendEvent(NewLoginError(LoginErrorType_TFA_ERROR, ""))
return
}
if err == pmapi.ErrBad2FACode {
s.log.Warn("Login 2FA: abort 2fa")
_ = s.SendEvent(NewLoginError(LoginErrorType_TFA_ABORT, ""))
s.loginClean()
return
}
if err != nil {
s.log.WithError(err).Warn("Login 2FA: failed.")
_ = s.SendEvent(NewLoginError(LoginErrorType_TFA_ABORT, err.Error()))
s.loginClean()
return
}
if s.auth.HasMailboxPassword() {
_ = s.SendEvent(NewLoginTwoPasswordsRequestedEvent())
return
}
s.finishLogin()
}()
return &emptypb.Empty{}, nil
}
func (s *Service) Login2Passwords(_ context.Context, login *LoginRequest) (*emptypb.Empty, error) {
s.log.WithField("username", login.Username).Info("Login2Passwords")
go func() {
defer s.panicHandler.HandlePanic()
var err error
s.password, err = base64.StdEncoding.DecodeString(login.Password)
if err != nil {
s.log.WithError(err).Error("Cannot decode mbox password")
_ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, "Cannot decode mbox password"))
s.loginClean()
return
}
s.finishLogin()
}()
return &emptypb.Empty{}, nil
}
func (s *Service) LoginAbort(_ context.Context, loginAbort *LoginAbortRequest) (*emptypb.Empty, error) {
s.log.WithField("username", loginAbort.Username).Info("LoginAbort")
go func() {
defer s.panicHandler.HandlePanic()
s.loginAbort()
}()
return &emptypb.Empty{}, nil
}
func (s *Service) CheckUpdate(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
s.log.Info("CheckUpdate")
// TO-DO GODT-1670 Implement update check
return &emptypb.Empty{}, nil
}
func (s *Service) InstallUpdate(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
s.log.Info("InstallUpdate")
// TO-DO GODT-1670 Implement update install
return &emptypb.Empty{}, nil
}
func (s *Service) SetIsAutomaticUpdateOn(_ context.Context, isOn *wrapperspb.BoolValue) (*emptypb.Empty, error) {
s.log.WithField("isOn", isOn.Value).Info("SetIsAutomaticUpdateOn")
currentlyOn := s.settings.GetBool(settings.AutoUpdateKey)
if currentlyOn == isOn.Value {
return &emptypb.Empty{}, nil
}
s.settings.SetBool(settings.AutoUpdateKey, isOn.Value)
s.checkUpdateAndNotify()
return &emptypb.Empty{}, nil
}
func (s *Service) IsAutomaticUpdateOn(context.Context, *emptypb.Empty) (*wrapperspb.BoolValue, error) {
s.log.Info("IsAutomaticUpdateOn")
return wrapperspb.Bool(s.settings.GetBool(settings.AutoUpdateKey)), nil
}
func (s *Service) IsCacheOnDiskEnabled(context.Context, *emptypb.Empty) (*wrapperspb.BoolValue, error) {
s.log.Info("IsCacheOnDiskEnabled")
return wrapperspb.Bool(s.settings.GetBool(settings.CacheEnabledKey)), nil
}
func (s *Service) DiskCachePath(context.Context, *emptypb.Empty) (*wrapperspb.StringValue, error) {
s.log.Info("DiskCachePath")
return wrapperspb.String(s.settings.Get(settings.CacheLocationKey)), nil
}
func (s *Service) ChangeLocalCache(_ context.Context, change *ChangeLocalCacheRequest) (*emptypb.Empty, error) {
s.log.WithField("enableDiskCache", change.EnableDiskCache).
WithField("diskCachePath", change.DiskCachePath).
Info("DiskCachePath")
defer func() { _ = s.SendEvent(NewCacheChangeLocalCacheFinishedEvent()) }()
defer func() { _ = s.SendEvent(NewIsCacheOnDiskEnabledChanged(s.settings.GetBool(settings.CacheEnabledKey))) }()
defer func() { _ = s.SendEvent(NewDiskCachePathChanged(s.settings.Get(settings.CacheCompressionKey))) }()
if change.EnableDiskCache != s.settings.GetBool(settings.CacheEnabledKey) {
if change.EnableDiskCache {
if err := s.bridge.EnableCache(); err != nil {
s.log.WithError(err).Error("Cannot enable disk cache")
}
} else {
if err := s.bridge.DisableCache(); err != nil {
s.log.WithError(err).Error("Cannot disable disk cache")
}
}
}
path := change.DiskCachePath
//goland:noinspection GoBoolExpressions
if (runtime.GOOS == "windows") && (path[0] == '/') {
path = path[1:]
}
if change.EnableDiskCache && path != s.settings.Get(settings.CacheLocationKey) {
if err := s.bridge.MigrateCache(s.settings.Get(settings.CacheLocationKey), path); err != nil {
s.log.WithError(err).Error("The local cache location could not be changed.")
_ = s.SendEvent(NewCacheErrorEvent(CacheErrorType_CACHE_CANT_MOVE_ERROR))
return &emptypb.Empty{}, nil
}
s.settings.Set(settings.CacheLocationKey, path)
}
_ = s.SendEvent(NewCacheLocationChangeSuccessEvent())
s.restart()
return &emptypb.Empty{}, nil
}
func (s *Service) SetIsDoHEnabled(_ context.Context, isEnabled *wrapperspb.BoolValue) (*emptypb.Empty, error) {
s.log.WithField("isEnabled", isEnabled.Value).Info("SetIsDohEnabled")
s.bridge.SetProxyAllowed(isEnabled.Value)
return &emptypb.Empty{}, nil
}
func (s *Service) IsDoHEnabled(context.Context, *emptypb.Empty) (*wrapperspb.BoolValue, error) {
s.log.Info("IsDohEnabled")
return wrapperspb.Bool(s.bridge.GetProxyAllowed()), nil
}
func (s *Service) SetUseSslForSmtp(_ context.Context, useSsl *wrapperspb.BoolValue) (*emptypb.Empty, error) { //nolint:revive,stylecheck
s.log.WithField("useSsl", useSsl.Value).Info("SetUseSslForSmtp")
if s.settings.GetBool(settings.SMTPSSLKey) == useSsl.Value {
return &emptypb.Empty{}, nil
}
defer func() { _ = s.SendEvent(NewMailSettingsUseSslForSmtpFinishedEvent()) }()
s.settings.SetBool(settings.SMTPSSLKey, useSsl.Value)
s.restart()
return &emptypb.Empty{}, nil
}
func (s *Service) UseSslForSmtp(context.Context, *emptypb.Empty) (*wrapperspb.BoolValue, error) { //nolint:revive,stylecheck
s.log.Info("UseSslForSmtp")
return wrapperspb.Bool(s.settings.GetBool(settings.SMTPSSLKey)), nil
}
func (s *Service) Hostname(context.Context, *emptypb.Empty) (*wrapperspb.StringValue, error) {
s.log.Info("Hostname")
return wrapperspb.String(bridge.Host), nil
}
func (s *Service) ImapPort(context.Context, *emptypb.Empty) (*wrapperspb.Int32Value, error) {
s.log.Info("ImapPort")
return wrapperspb.Int32(int32(s.settings.GetInt(settings.IMAPPortKey))), nil
}
func (s *Service) SmtpPort(context.Context, *emptypb.Empty) (*wrapperspb.Int32Value, error) { //nolint:revive,stylecheck
s.log.Info("SmtpPort")
return wrapperspb.Int32(int32(s.settings.GetInt(settings.SMTPPortKey))), nil
}
func (s *Service) ChangePorts(_ context.Context, ports *ChangePortsRequest) (*emptypb.Empty, error) {
s.log.WithField("imapPort", ports.ImapPort).WithField("smtpPort", ports.SmtpPort).Info("ChangePorts")
defer func() { _ = s.SendEvent(NewMailSettingsChangePortFinishedEvent()) }()
s.settings.SetInt(settings.IMAPPortKey, int(ports.ImapPort))
s.settings.SetInt(settings.SMTPPortKey, int(ports.SmtpPort))
s.restart()
return &emptypb.Empty{}, nil
}
func (s *Service) IsPortFree(_ context.Context, port *wrapperspb.Int32Value) (*wrapperspb.BoolValue, error) {
s.log.Info("IsPortFree")
return wrapperspb.Bool(ports.IsPortFree(int(port.Value))), nil
}
func (s *Service) AvailableKeychains(context.Context, *emptypb.Empty) (*AvailableKeychainsResponse, error) {
s.log.Info("AvailableKeychains")
keychains := make([]string, 0, len(keychain.Helpers))
for chain := range keychain.Helpers {
keychains = append(keychains, chain)
}
return &AvailableKeychainsResponse{Keychains: keychains}, nil
}
func (s *Service) SetCurrentKeychain(_ context.Context, keychain *wrapperspb.StringValue) (*emptypb.Empty, error) {
s.log.WithField("keychain", keychain.Value).Info("SetCurrentKeyChain") // we do not check validity.
defer func() { _ = s.SendEvent(NewKeychainChangeKeychainFinishedEvent()) }()
if s.bridge.GetKeychainApp() == keychain.Value {
return &emptypb.Empty{}, nil
}
s.bridge.SetKeychainApp(keychain.Value)
return &emptypb.Empty{}, nil
}
func (s *Service) CurrentKeychain(context.Context, *emptypb.Empty) (*wrapperspb.StringValue, error) {
s.log.Info("CurrentKeychain")
return wrapperspb.String(s.bridge.GetKeychainApp()), nil
}

View File

@ -0,0 +1,151 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.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 <https://www.gnu.org/licenses/>.
package grpc
import (
"context"
"github.com/pkg/errors"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
// StartEventStream implement the gRPC server->Client event stream.
func (s *Service) StartEventStream(_ *emptypb.Empty, server Bridge_StartEventStreamServer) error {
s.log.Info("Starting Event stream")
if s.eventStreamCh != nil {
return status.Errorf(codes.AlreadyExists, "the service is already streaming") // TO-DO GODT-1667 decide if we want to kill the existing stream.
}
s.eventStreamCh = make(chan *StreamEvent)
s.eventStreamDoneCh = make(chan struct{})
// TO-DO GODT-1667 We should have a safer we to close this channel? What if an event occur while we are closing?
defer func() {
close(s.eventStreamCh)
s.eventStreamCh = nil
close(s.eventStreamDoneCh)
s.eventStreamDoneCh = nil
}()
for {
select {
case <-s.eventStreamDoneCh:
s.log.Info("Stop Event stream")
return nil
case event := <-s.eventStreamCh:
s.log.WithField("event", event).Info("Sending event")
if err := server.Send(event); err != nil {
s.log.Info("Stop Event stream")
return err
}
}
}
}
// StopEventStream stops the event stream.
func (s *Service) StopEventStream(_ context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
if s.eventStreamCh == nil {
return nil, status.Errorf(codes.NotFound, "The service is not streaming")
}
s.eventStreamDoneCh <- struct{}{}
return &emptypb.Empty{}, nil
}
// SendEvent sends an event to the via the gRPC event stream.
func (s *Service) SendEvent(event *StreamEvent) error {
if s.eventStreamCh == nil {
return errors.New("gRPC service is not streaming")
}
s.eventStreamCh <- event
return nil
}
// StartEventTest sends all the known event via gRPC.
func (s *Service) StartEventTest() error { //nolint:funlen
const dummyAddress = "dummy@proton.me"
events := []*StreamEvent{
// app
NewInternetStatusEvent(true),
NewToggleAutostartFinishedEvent(),
NewResetFinishedEvent(),
NewReportBugFinishedEvent(),
NewReportBugSuccessEvent(),
NewReportBugErrorEvent(),
NewShowMainWindowEvent(),
// login
NewLoginError(LoginErrorType_FREE_USER, "error"),
NewLoginTfaRequestedEvent(dummyAddress),
NewLoginTwoPasswordsRequestedEvent(),
NewLoginFinishedEvent("userID"),
NewLoginAlreadyLoggedInEvent("userID"),
// update
NewUpdateErrorEvent(UpdateErrorType_UPDATE_SILENT_ERROR),
NewUpdateManualReadyEvent("2.0"),
NewUpdateManualRestartNeededEvent(),
NewUpdateForceEvent("2.0"),
NewUpdateSilentRestartNeededEvent(),
NewUpdateIsLatestVersionEvent(),
NewUpdateCheckFinishedEvent(),
// cache
NewCacheErrorEvent(CacheErrorType_CACHE_UNAVAILABLE_ERROR),
NewCacheLocationChangeSuccessEvent(),
NewCacheChangeLocalCacheFinishedEvent(),
NewIsCacheOnDiskEnabledChanged(true),
NewDiskCachePathChanged("/dummy/path"),
// mail settings
NewMailSettingsErrorEvent(MailSettingsErrorType_IMAP_PORT_ISSUE),
NewMailSettingsUseSslForSmtpFinishedEvent(),
NewMailSettingsChangePortFinishedEvent(),
// keychain
NewKeychainChangeKeychainFinishedEvent(),
NewKeychainHasNoKeychainEvent(),
NewKeychainRebuildKeychainEvent(),
// mail
NewMailNoActiveKeyForRecipientEvent(dummyAddress),
NewMailAddressChangeEvent(dummyAddress),
NewMailAddressChangeLogoutEvent(dummyAddress),
NewMailApiCertIssue(),
// user
NewUserToggleSplitModeFinishedEvent("userID"),
NewUserDisconnectedEvent("username"),
NewUserChangedEvent("userID"),
}
for _, event := range events {
if err := s.SendEvent(event); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,132 @@
// Copyright (c) 2022 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 <https://www.gnu.org/licenses/>.
package grpc
import (
"context"
"time"
"github.com/ProtonMail/proton-bridge/v2/internal/frontend/clientconfig"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
func (s *Service) GetUserList(context.Context, *emptypb.Empty) (*UserListResponse, error) {
s.log.Info("GetUserList")
users := s.bridge.GetUsers()
userList := make([]*User, len(users))
for i, user := range users {
userList[i] = grpcUserFromBridge(user)
}
// If there are no active accounts.
if len(userList) == 0 {
s.log.Info("No active accounts")
}
return &UserListResponse{Users: userList}, nil
}
func (s *Service) GetUser(_ context.Context, userID *wrapperspb.StringValue) (*User, error) {
s.log.WithField("userID", userID).Info("GetUser")
user, err := s.bridge.GetUser(userID.Value)
if err != nil {
return nil, status.Errorf(codes.NotFound, "user not found %v", userID.Value)
}
return grpcUserFromBridge(user), nil
}
func (s *Service) SetUserSplitMode(_ context.Context, splitMode *UserSplitModeRequest) (*emptypb.Empty, error) {
s.log.WithField("UserID", splitMode.UserID).WithField("Active", splitMode.Active).Info("SetUserSplitMode")
user, err := s.bridge.GetUser(splitMode.UserID)
if err != nil {
return nil, status.Errorf(codes.NotFound, "user not found %v", splitMode.UserID)
}
go func() {
defer s.panicHandler.HandlePanic()
defer func() { _ = s.SendEvent(NewUserToggleSplitModeFinishedEvent(splitMode.UserID)) }()
if splitMode.Active == user.IsCombinedAddressMode() {
_ = user.SwitchAddressMode() // check for errors
}
}()
return &emptypb.Empty{}, nil
}
func (s *Service) LogoutUser(_ context.Context, userID *wrapperspb.StringValue) (*emptypb.Empty, error) {
s.log.WithField("UserID", userID.Value).Info("LogoutUser")
user, err := s.bridge.GetUser(userID.Value)
if err != nil {
return nil, status.Errorf(codes.NotFound, "user not found %v", userID.Value)
}
go func() {
defer s.panicHandler.HandlePanic()
_ = user.Logout()
}()
return &emptypb.Empty{}, nil
}
func (s *Service) RemoveUser(_ context.Context, userID *wrapperspb.StringValue) (*emptypb.Empty, error) {
s.log.WithField("UserID", userID.Value).Info("RemoveUser")
go func() {
defer s.panicHandler.HandlePanic()
// remove preferences
if err := s.bridge.DeleteUser(userID.Value, false); err != nil {
s.log.WithError(err).Error("Failed to remove user")
// notification
}
}()
return &emptypb.Empty{}, nil
}
func (s *Service) ConfigureUserAppleMail(_ context.Context, request *ConfigureAppleMailRequest) (*emptypb.Empty, error) {
s.log.WithField("UserID", request.UserID).WithField("Address", request.Address).Info("ConfigureUserAppleMail")
user, err := s.bridge.GetUser(request.UserID)
if err != nil {
s.log.WithField("userID", request.UserID).Error("Cannot configure AppleMail for user")
return nil, status.Error(codes.NotFound, "Cannot configure AppleMail for user")
}
needRestart, err := clientconfig.ConfigureAppleMail(user, request.Address, s.settings)
if err != nil {
s.log.WithError(err).Error("Apple Mail config failed")
return nil, status.Error(codes.Internal, "Apple Mail config failed")
}
if needRestart {
// There is delay needed for external window to open
time.Sleep(2 * time.Second)
s.restart()
}
return &emptypb.Empty{}, nil
}

View File

@ -0,0 +1,73 @@
// Copyright (c) 2022 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 <https://www.gnu.org/licenses/>.
package grpc
import (
"regexp"
"strings"
"github.com/ProtonMail/proton-bridge/v2/internal/frontend/types"
)
var (
reMultiSpaces = regexp.MustCompile(`\s{2,}`)
reStartWithSymbol = regexp.MustCompile(`^[.,/#!$@%^&*;:{}=\-_` + "`" + `~()]`)
)
// getInitials based on webapp implementation:
// https://github.com/ProtonMail/WebClients/blob/55d96a8b4afaaa4372fc5f1ef34953f2070fd7ec/packages/shared/lib/helpers/string.ts#L145
func getInitials(fullName string) string {
words := strings.Split(
reMultiSpaces.ReplaceAllString(fullName, " "),
" ",
)
n := 0
for _, word := range words {
if !reStartWithSymbol.MatchString(word) {
words[n] = word
n++
}
}
if n == 0 {
return "?"
}
initials := words[0][0:1]
if n != 1 {
initials += words[n-1][0:1]
}
return strings.ToUpper(initials)
}
// grpcUserFromBridge converts a bridge user to a gRPC user.
func grpcUserFromBridge(user types.User) *User {
return &User{
Id: user.ID(),
Username: user.Username(),
AvatarText: getInitials(user.Username()),
LoggedIn: user.IsConnected(),
SplitMode: user.IsCombinedAddressMode(),
SetupGuideSeen: true, // users listed have already seen the setup guide.
UsedBytes: user.UsedBytes(),
TotalBytes: user.TotalBytes(),
Password: user.GetBridgePassword(),
Addresses: user.GetAddresses(),
}
}