forked from Silverfish/proton-bridge
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:
5128
internal/frontend/grpc/bridge.pb.go
Normal file
5128
internal/frontend/grpc/bridge.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
424
internal/frontend/grpc/bridge.proto
Normal file
424
internal/frontend/grpc/bridge.proto
Normal 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;
|
||||
}
|
||||
|
||||
1955
internal/frontend/grpc/bridge_grpc.pb.go
Normal file
1955
internal/frontend/grpc/bridge_grpc.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
69
internal/frontend/grpc/certs.go
Normal file
69
internal/frontend/grpc/certs.go
Normal 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-----`
|
||||
)
|
||||
199
internal/frontend/grpc/event_factory.go
Normal file
199
internal/frontend/grpc/event_factory.go
Normal 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}}
|
||||
}
|
||||
329
internal/frontend/grpc/service.go
Normal file
329
internal/frontend/grpc/service.go
Normal 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.
|
||||
}
|
||||
543
internal/frontend/grpc/service_methods.go
Normal file
543
internal/frontend/grpc/service_methods.go
Normal 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
|
||||
}
|
||||
151
internal/frontend/grpc/service_stream.go
Normal file
151
internal/frontend/grpc/service_stream.go
Normal 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
|
||||
}
|
||||
132
internal/frontend/grpc/service_user.go
Normal file
132
internal/frontend/grpc/service_user.go
Normal 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
|
||||
}
|
||||
73
internal/frontend/grpc/utils.go
Normal file
73
internal/frontend/grpc/utils.go
Normal 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(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user