From 543c35041df2e82e33fba47a9d4f68f7fbc7c625 Mon Sep 17 00:00:00 2001 From: Romain Le Jeune Date: Tue, 2 May 2023 07:13:21 +0000 Subject: [PATCH 01/43] fix(GODT-2464): Filter attachment name from content-type parameter to not send it twice to the API. --- internal/user/smtp.go | 4 + pkg/message/parser_test.go | 34 +++++ .../text_plain_docx_attachment_cyrillic.eml | 24 +++ .../text_plain_pdf_attachment_cyrillic.eml | 25 +++ tests/bdd_test.go | 1 + tests/environment_test.go | 24 +++ tests/features/smtp/send/attachment.feature | 143 ++++++++++++++++++ 7 files changed, 255 insertions(+) create mode 100644 pkg/message/testdata/text_plain_docx_attachment_cyrillic.eml create mode 100644 pkg/message/testdata/text_plain_pdf_attachment_cyrillic.eml create mode 100644 tests/features/smtp/send/attachment.feature diff --git a/internal/user/smtp.go b/internal/user/smtp.go index f5f6d2b8..7351c2ca 100644 --- a/internal/user/smtp.go +++ b/internal/user/smtp.go @@ -438,6 +438,10 @@ func (user *User) createAttachments( } } + // Exclude name from params since this is already provided using Filename. + delete(att.MIMEParams, "name") + delete(att.MIMEParams, "filename") + attachment, err := client.UploadAttachment(ctx, addrKR, proton.CreateAttachmentReq{ Filename: att.Name, MessageID: draftID, diff --git a/pkg/message/parser_test.go b/pkg/message/parser_test.go index 5bbce163..de28aed0 100644 --- a/pkg/message/parser_test.go +++ b/pkg/message/parser_test.go @@ -673,6 +673,40 @@ func TestParsePanic(t *testing.T) { require.Error(t, err) } +func TestParseTextPlainWithPdfttachmentCyrillic(t *testing.T) { + f := getFileReader("text_plain_pdf_attachment_cyrillic.eml") + + m, err := Parse(f) + require.NoError(t, err) + + assert.Equal(t, `"Sender" `, m.Sender.String()) + assert.Equal(t, `"Receiver" `, m.ToList[0].String()) + + assert.Equal(t, "Shake that body", string(m.RichBody)) + assert.Equal(t, "Shake that body", string(m.PlainBody)) + + require.Len(t, m.Attachments, 1) + require.Equal(t, "application/pdf", m.Attachments[0].MIMEType) + assert.Equal(t, "АБВГДЃЕЖЗЅИЈКЛЉМНЊОПРСТЌУФХЧЏЗШ.pdf", m.Attachments[0].Name) +} + +func TestParseTextPlainWithDocxAttachmentCyrillic(t *testing.T) { + f := getFileReader("text_plain_docx_attachment_cyrillic.eml") + + m, err := Parse(f) + require.NoError(t, err) + + assert.Equal(t, `"Sender" `, m.Sender.String()) + assert.Equal(t, `"Receiver" `, m.ToList[0].String()) + + assert.Equal(t, "Shake that body", string(m.RichBody)) + assert.Equal(t, "Shake that body", string(m.PlainBody)) + + require.Len(t, m.Attachments, 1) + require.Equal(t, "application/vnd.openxmlformats-officedocument.wordprocessingml.document", m.Attachments[0].MIMEType) + assert.Equal(t, "АБВГДЃЕЖЗЅИЈКЛЉМНЊОПРСТЌУФХЧЏЗШ.docx", m.Attachments[0].Name) +} + func getFileReader(filename string) io.Reader { f, err := os.Open(filepath.Join("testdata", filename)) if err != nil { diff --git a/pkg/message/testdata/text_plain_docx_attachment_cyrillic.eml b/pkg/message/testdata/text_plain_docx_attachment_cyrillic.eml new file mode 100644 index 00000000..b81cfd1e --- /dev/null +++ b/pkg/message/testdata/text_plain_docx_attachment_cyrillic.eml @@ -0,0 +1,24 @@ +Content-Type: multipart/mixed; boundary="------------nq8WTMHkJcymWO6pWfby0uY3" +To: "Receiver" +From: "Sender" +Subject: Test with cyrillic attachment + +--------------nq8WTMHkJcymWO6pWfby0uY3 +Content-Type: text/plain; charset=UTF-8; format=flowed +Content-Transfer-Encoding: 7bit + +Shake that body +--------------nq8WTMHkJcymWO6pWfby0uY3 +Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document; + name="=?UTF-8?B?0JDQkdCS0JPQlNCD0JXQltCX0IXQmNCI0JrQm9CJ0JzQndCK0J7Qn9Cg?= + =?UTF-8?B?0KHQotCM0KPQpNCl0KfQj9CX0KguZG9jeA==?=" +Content-Disposition: attachment; + filename*0*=UTF-8''%D0%90%D0%91%D0%92%D0%93%D0%94%D0%83%D0%95%D0%96%D0%97; + filename*1*=%D0%85%D0%98%D0%88%D0%9A%D0%9B%D0%89%D0%9C%D0%9D%D0%8A%D0%9E; + filename*2*=%D0%9F%D0%A0%D0%A1%D0%A2%D0%8C%D0%A3%D0%A4%D0%A5%D0%A7%D0%8F; + filename*3*=%D0%97%D0%A8%2E%64%6F%63%78 +Content-Transfer-Encoding: base64 + +0JDQkdCS0JPQlNCD0JXQltCX0IXQmNCI0JrQm9CJ0JzQndCK0J7Qn9Cg0KHQotCM0KPQpNCl0KfQj9CX0Kg= + +--------------nq8WTMHkJcymWO6pWfby0uY3-- diff --git a/pkg/message/testdata/text_plain_pdf_attachment_cyrillic.eml b/pkg/message/testdata/text_plain_pdf_attachment_cyrillic.eml new file mode 100644 index 00000000..e83c92b6 --- /dev/null +++ b/pkg/message/testdata/text_plain_pdf_attachment_cyrillic.eml @@ -0,0 +1,25 @@ +Content-Type: multipart/mixed; boundary="------------bYzsV6z0EdKTbltmCDZgIM15" +To: "Receiver" +From: "Sender" +Subject: Test with cyrillic attachment + +--------------bYzsV6z0EdKTbltmCDZgIM15 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=utf-8 + +Shake that body +--------------bYzsV6z0EdKTbltmCDZgIM15 +Content-Type: application/pdf; + name="=?UTF-8?B?0JDQkdCS0JPQlNCD0JXQltCX0IXQmNCI0JrQm9CJ0JzQndCK0J7Qn9Cg?= + =?UTF-8?B?0KHQotCM0KPQpNCl0KfQj9CX0KgucGRm?=" +Content-Disposition: attachment; + filename*0*=UTF-8''%D0%90%D0%91%D0%92%D0%93%D0%94%D0%83%D0%95%D0%96%D0%97; + filename*1*=%D0%85%D0%98%D0%88%D0%9A%D0%9B%D0%89%D0%9C%D0%9D%D0%8A%D0%9E; + filename*2*=%D0%9F%D0%A0%D0%A1%D0%A2%D0%8C%D0%A3%D0%A4%D0%A5%D0%A7%D0%8F; + filename*3*=%D0%97%D0%A8%2E%70%64%66 +Content-Transfer-Encoding: base64 + +0JDQkdCS0JPQlNCD0JXQltCX0IXQmNCI0JrQm9CJ0JzQndCK0J7Qn9Cg0KHQotCM0KPQpNCl0KfQj9CX0Kg= + + +--------------bYzsV6z0EdKTbltmCDZgIM15-- diff --git a/tests/bdd_test.go b/tests/bdd_test.go index 92a73bed..1dadb870 100644 --- a/tests/bdd_test.go +++ b/tests/bdd_test.go @@ -106,6 +106,7 @@ func TestFeatures(testingT *testing.T) { ctx.Step(`^the user agent is "([^"]*)"$`, s.theUserAgentIs) ctx.Step(`^the header in the "([^"]*)" request to "([^"]*)" has "([^"]*)" set to "([^"]*)"$`, s.theHeaderInTheRequestToHasSetTo) ctx.Step(`^the body in the "([^"]*)" request to "([^"]*)" is:$`, s.theBodyInTheRequestToIs) + ctx.Step(`^the body in the "([^"]*)" response to "([^"]*)" is:$`, s.theBodyInTheResponseToIs) ctx.Step(`^the API requires bridge version at least "([^"]*)"$`, s.theAPIRequiresBridgeVersion) ctx.Step(`^the network port (\d+) is busy$`, s.networkPortIsBusy) ctx.Step(`^the network port range (\d+)-(\d+) is busy$`, s.networkPortRangeIsBusy) diff --git a/tests/environment_test.go b/tests/environment_test.go index da78767d..d80a7861 100644 --- a/tests/environment_test.go +++ b/tests/environment_test.go @@ -110,3 +110,27 @@ func (s *scenario) theBodyInTheRequestToIs(method, path string, value *godog.Doc return nil } + +func (s *scenario) theBodyInTheResponseToIs(method, path string, value *godog.DocString) error { + // We have to exclude HTTP-Overrides to avoid race condition with the creating and sending of the draft message. + call, err := s.t.getLastCallExcludingHTTPOverride(method, path) + if err != nil { + return err + } + + var body, want map[string]any + + if err := json.Unmarshal(call.ResponseBody, &body); err != nil { + return err + } + + if err := json.Unmarshal([]byte(value.Content), &want); err != nil { + return err + } + + if !IsSub(body, want) { + return fmt.Errorf("have body %v, want %v", body, want) + } + + return nil +} diff --git a/tests/features/smtp/send/attachment.feature b/tests/features/smtp/send/attachment.feature new file mode 100644 index 00000000..d8151a3c --- /dev/null +++ b/tests/features/smtp/send/attachment.feature @@ -0,0 +1,143 @@ +Feature: SMTP sending with attachment + Background: + Given there exists an account with username "[user:user1]" and password "password" + And there exists an account with username "[user:user2]" and password "password" + Then it succeeds + When bridge starts + And the user logs in with username "[user:user1]" and password "password" + And user "[user:user1]" finishes syncing + Then it succeeds + When user "[user:user1]" connects and authenticates SMTP client "1" + And user "[user:user1]" connects and authenticates IMAP client "1" + Then it succeeds + + @long-black + Scenario: Sending with cyrillic PDF attachment + When SMTP client "1" sends the following message from "[user:user1]@[domain]" to "[user:user2]@[domain]": + """ + Content-Type: multipart/mixed; boundary="------------bYzsV6z0EdKTbltmCDZgIM15" + From: Bridge Test <[user:user1]@[domain]> + To: Internal Bridge <[user:user2]@[domain]> + Subject: Test with cyrillic attachment + + --------------bYzsV6z0EdKTbltmCDZgIM15 + Content-Transfer-Encoding: quoted-printable + Content-Type: text/plain; charset=utf-8 + + Shake that body + --------------bYzsV6z0EdKTbltmCDZgIM15 + Content-Type: application/pdf; + name="=?UTF-8?B?0JDQkdCS0JPQlNCD0JXQltCX0IXQmNCI0JrQm9CJ0JzQndCK0J7Qn9Cg?= + =?UTF-8?B?0KHQotCM0KPQpNCl0KfQj9CX0KgucGRm?=" + Content-Disposition: attachment; + filename*0*=UTF-8''%D0%90%D0%91%D0%92%D0%93%D0%94%D0%83%D0%95%D0%96%D0%97; + filename*1*=%D0%85%D0%98%D0%88%D0%9A%D0%9B%D0%89%D0%9C%D0%9D%D0%8A%D0%9E; + filename*2*=%D0%9F%D0%A0%D0%A1%D0%A2%D0%8C%D0%A3%D0%A4%D0%A5%D0%A7%D0%8F; + filename*3*=%D0%97%D0%A8%2E%70%64%66 + Content-Transfer-Encoding: base64 + + 0JDQkdCS0JPQlNCD0JXQltCX0IXQmNCI0JrQm9CJ0JzQndCK0J7Qn9Cg0KHQotCM0KPQpNCl0KfQj9CX0Kg= + + --------------bYzsV6z0EdKTbltmCDZgIM15-- + + """ + Then it succeeds + Then IMAP client "1" eventually sees the following messages in "Sent": + | from | to | subject | + | [user:user1]@[domain] | [user:user2]@[domain] | Test with cyrillic attachment | + And the body in the "POST" request to "/mail/v4/messages" is: + """ + { + "Message": { + "Subject": "Test with cyrillic attachment", + "Sender": { + "Name": "Bridge Test" + }, + "ToList": [ + { + "Address": "[user:user2]@[domain]", + "Name": "Internal Bridge" + } + ], + "CCList": [], + "BCCList": [], + "MIMEType": "text/plain" + } + } + """ + And the body in the "POST" response to "/mail/v4/attachments" is: + """ + { + "Attachment":{ + "Name": "АБВГДЃЕЖЗЅИЈКЛЉМНЊОПРСТЌУФХЧЏЗШ.pdf", + "MIMEType": "application/pdf", + "Disposition": "attachment" + } + } + """ + + + @long-black + Scenario: Sending with cyrillic docx attachment + When SMTP client "1" sends the following message from "[user:user1]@[domain]" to "[user:user2]@[domain]": + """ + Content-Type: multipart/mixed; boundary="------------9xfXriG1c1v5iJlMiIMCaIWP" + From: Bridge Test <[user:user1]@[domain]> + To: Internal Bridge <[user:user2]@[domain]> + Subject: Test with cyrillic attachment + + --------------9xfXriG1c1v5iJlMiIMCaIWP + Content-Type: text/plain; charset=UTF-8; format=flowed + Content-Transfer-Encoding: 7bit + + Shake that body + --------------9xfXriG1c1v5iJlMiIMCaIWP + Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document; + name="=?UTF-8?B?0JDQkdCS0JPQlNCD0JXQltCX0IXQmNCI0JrQm9CJ0JzQndCK0J7Qn9Cg?= + =?UTF-8?B?0KHQotCM0KPQpNCl0KfQj9CX0KguZG9jeA==?=" + Content-Disposition: attachment; + filename*0*=UTF-8''%D0%90%D0%91%D0%92%D0%93%D0%94%D0%83%D0%95%D0%96%D0%97; + filename*1*=%D0%85%D0%98%D0%88%D0%9A%D0%9B%D0%89%D0%9C%D0%9D%D0%8A%D0%9E; + filename*2*=%D0%9F%D0%A0%D0%A1%D0%A2%D0%8C%D0%A3%D0%A4%D0%A5%D0%A7%D0%8F; + filename*3*=%D0%97%D0%A8%2E%64%6F%63%78 + Content-Transfer-Encoding: base64 + + 0JDQkdCS0JPQlNCD0JXQltCX0IXQmNCI0JrQm9CJ0JzQndCK0J7Qn9Cg0KHQotCM0KPQpNCl0KfQj9CX0Kg= + + --------------9xfXriG1c1v5iJlMiIMCaIWP-- + + """ + Then it succeeds + Then IMAP client "1" eventually sees the following messages in "Sent": + | from | to | subject | + | [user:user1]@[domain] | [user:user2]@[domain] | Test with cyrillic attachment | + And the body in the "POST" request to "/mail/v4/messages" is: + """ + { + "Message": { + "Subject": "Test with cyrillic attachment", + "Sender": { + "Name": "Bridge Test" + }, + "ToList": [ + { + "Address": "[user:user2]@[domain]", + "Name": "Internal Bridge" + } + ], + "CCList": [], + "BCCList": [], + "MIMEType": "text/plain" + } + } + """ + And the body in the "POST" response to "/mail/v4/attachments" is: + """ + { + "Attachment":{ + "Name": "АБВГДЃЕЖЗЅИЈКЛЉМНЊОПРСТЌУФХЧЏЗШ.docx", + "MIMEType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "Disposition": "attachment" + } + } + """ \ No newline at end of file From 6a6ead8e6dc33338835346003b5661888a15c4f4 Mon Sep 17 00:00:00 2001 From: Xavier Michelon Date: Tue, 25 Apr 2023 18:39:23 +0200 Subject: [PATCH 02/43] feat(GODT-2540): notify user of wrong IMAP password. --- .../bridge-gui/bridge-gui/CMakeLists.txt | 3 +- internal/frontend/bridge-gui/bridge-gui/Pch.h | 1 + .../bridge-gui/bridge-gui/QMLBackend.cpp | 33 ++++++-- .../bridge-gui/bridge-gui/QMLBackend.h | 4 +- .../bridge-gui/bridge-gui/TrayIcon.cpp | 81 +++++++++++++++++-- .../frontend/bridge-gui/bridge-gui/TrayIcon.h | 4 +- .../bridge-gui/bridge-gui/qml/MainWindow.qml | 6 +- .../bridgepp/bridgepp/User/User.cpp | 20 ++--- .../bridge-gui/bridgepp/bridgepp/User/User.h | 12 ++- 9 files changed, 128 insertions(+), 36 deletions(-) diff --git a/internal/frontend/bridge-gui/bridge-gui/CMakeLists.txt b/internal/frontend/bridge-gui/bridge-gui/CMakeLists.txt index 77f06e49..2c14ed83 100644 --- a/internal/frontend/bridge-gui/bridge-gui/CMakeLists.txt +++ b/internal/frontend/bridge-gui/bridge-gui/CMakeLists.txt @@ -75,7 +75,7 @@ if(NOT UNIX) set(CMAKE_INSTALL_BINDIR ".") endif(NOT UNIX) -find_package(Qt6 COMPONENTS Core Quick Qml QuickControls2 Widgets REQUIRED) +find_package(Qt6 COMPONENTS Core Quick Qml QuickControls2 Widgets Svg REQUIRED) qt_standard_project_setup() set(CMAKE_AUTORCC ON) message(STATUS "Using Qt ${Qt6_VERSION}") @@ -147,6 +147,7 @@ target_link_libraries(bridge-gui Qt6::Quick Qt6::Qml Qt6::QuickControls2 + Qt6::Svg sentry::sentry bridgepp ) diff --git a/internal/frontend/bridge-gui/bridge-gui/Pch.h b/internal/frontend/bridge-gui/bridge-gui/Pch.h index b34e31d3..cb7bcb99 100644 --- a/internal/frontend/bridge-gui/bridge-gui/Pch.h +++ b/internal/frontend/bridge-gui/bridge-gui/Pch.h @@ -25,6 +25,7 @@ #include #include #include +#include #include diff --git a/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp b/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp index 2435426c..47e8e2db 100644 --- a/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp +++ b/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp @@ -994,15 +994,34 @@ void QMLBackend::onUserBadEvent(QString const &userID, QString const &) { void QMLBackend::onIMAPLoginFailed(QString const &username) { HANDLE_EXCEPTION( SPUser const user = users_->getUserWithUsernameOrEmail(username); - if ((!user) || (user->state() != UserState::SignedOut)) { // We want to pop-up only if a signed-out user has been detected + if (!user) { return; } - if (user->isInIMAPLoginFailureCooldown()) { - return; + + qint64 const cooldownDurationMs = 10 * 60 * 1000; // 10 minutes cooldown period for notifications + switch (user->state()) { + case UserState::SignedOut: + if (user->isNotificationInCooldown(User::ENotification::IMAPLoginWhileSignedOut)) { + return; + } + user->startNotificationCooldownPeriod(User::ENotification::IMAPLoginWhileSignedOut, cooldownDurationMs); + emit selectUser(user->id(), true); + emit imapLoginWhileSignedOut(username); + break; + + case UserState::Connected: + if (user->isNotificationInCooldown(User::ENotification::IMAPPasswordFailure)) { + return; + } + user->startNotificationCooldownPeriod(User::ENotification::IMAPPasswordFailure, cooldownDurationMs); + emit selectUser(user->id(), false); + trayIcon_->showErrorPopupNotification(tr("Incorrect password"), + tr("Your email client can't connect to Proton Bridge. Make sure you are using the local Bridge password shown in Bridge.")); + break; + + default: + break; } - user->startImapLoginFailureCooldown(60 * 60 * 1000); // 1 hour cooldown during which we will not display this notification to this user again. - emit selectUser(user->id()); - emit imapLoginWhileSignedOut(username); ) } @@ -1134,7 +1153,7 @@ void QMLBackend::displayBadEventDialog(QString const &userID) { emit userBadEvent(userID, tr("Bridge ran into an internal error and it is not able to proceed with the account %1. Synchronize your local database now or logout" " to do it later. Synchronization time depends on the size of your mailbox.").arg(elideLongString(user->primaryEmailOrUsername(), 30))); - emit selectUser(userID); + emit selectUser(userID, true); emit showMainWindow(); ) } diff --git a/internal/frontend/bridge-gui/bridge-gui/QMLBackend.h b/internal/frontend/bridge-gui/bridge-gui/QMLBackend.h index afde837e..7ee7e4f1 100644 --- a/internal/frontend/bridge-gui/bridge-gui/QMLBackend.h +++ b/internal/frontend/bridge-gui/bridge-gui/QMLBackend.h @@ -180,6 +180,8 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge void onVersionChanged(); ///< Slot for the version change signal. void setMailServerSettings(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP) const; ///< Forwards a connection mode change request from QML to gRPC void sendBadEventUserFeedback(QString const &userID, bool doResync); ///< Slot the providing user feedback for a bad event. + +public slots: // slots for functions that need to be processed locally. void setNormalTrayIcon(); ///< Set the tray icon to normal. void setErrorTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'error' state. void setWarnTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'warn' state. @@ -245,7 +247,7 @@ signals: // Signals received from the Go backend, to be forwarded to QML void hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event. void showHelp(); ///< Signal for the 'showHelp' event (from the context menu). void showSettings(); ///< Signal for the 'showHelp' event (from the context menu). - void selectUser(QString const& userID); ///< Signal emitted in order to selected a user with a given ID in the list. + void selectUser(QString const& userID, bool forceShowWindow); ///< Signal emitted in order to selected a user with a given ID in the list. void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event. void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account. diff --git a/internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp b/internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp index b389d89e..ccb3528b 100644 --- a/internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp +++ b/internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp @@ -49,6 +49,50 @@ QIcon loadIconFromImage(QString const &path) { } +//**************************************************************************************************************************************************** +/// \brief Load a multi-resolution icon from a SVG file. The image is assumed to be square. SVG is rasterized in 256, 128, 64, 32 and 16px. +/// +/// Note: QPixmap can load SVG files directly, but our SVG file are defined in small shape size and QPixmap will rasterize them a very low resolution +/// by default (eg. 16x16), which is insufficient for some uses. As a consequence, we manually generate a multi-resolution icon that render smoothly +/// at any acceptable resolution for an icon. +/// +/// \param[in] path The path of the SVG file. +/// \return The icon. +//**************************************************************************************************************************************************** +QIcon loadIconFromSVG(QString const &path, QColor const &color = QColor()) { + QSvgRenderer renderer(path); + QIcon icon; + qint32 size = 256; + + while (size >= 16) { + QPixmap pixmap(size, size); + pixmap.fill(QColor(0, 0, 0, 0)); + QPainter painter(&pixmap); + renderer.render(&painter); + if (color.isValid()) { + painter.setCompositionMode(QPainter::CompositionMode_SourceIn); + painter.fillRect(pixmap.rect(), color); + } + painter.end(); + icon.addPixmap(pixmap); + size /= 2; + } + + return icon; +} + + +//**************************************************************************************************************************************************** +// +//**************************************************************************************************************************************************** +QIcon loadIcon(QString const& path) { + if (path.endsWith(".svg", Qt::CaseInsensitive)) { + return loadIconFromSVG(path); + } + return loadIconFromImage(path); +} + + //**************************************************************************************************************************************************** /// \brief Retrieve the color associated with a tray icon state. /// @@ -95,6 +139,18 @@ QString stateText(TrayIcon::State state) { } +//**************************************************************************************************************************************************** +/// \brief converts a QML resource path to Qt resource path. +/// QML resource paths are a bit different from qt resource paths +/// \param[in] path The resource path. +/// \return +//**************************************************************************************************************************************************** +QString qmlResourcePathToQt(QString const &path) { + QString result = path; + result.replace(QRegularExpression(R"(^\.\/)"), ":/qml/"); + return result; +} + } // anonymous namespace @@ -111,7 +167,8 @@ TrayIcon::TrayIcon() connect(menu_.get(), &QMenu::aboutToShow, this, &TrayIcon::onMenuAboutToShow); connect(this, &TrayIcon::selectUser, &app().backend(), &QMLBackend::selectUser); connect(this, &TrayIcon::activated, this, &TrayIcon::onActivated); - + // some OSes/Desktop managers will automatically show main window when clicked, but not all, so we do it manually. + connect(this, &TrayIcon::messageClicked, &app().backend(), &QMLBackend::showMainWindow); this->show(); this->setState(State::Normal, QString(), QString()); @@ -151,7 +208,7 @@ void TrayIcon::onUserClicked() { throw Exception("Could not retrieve context menu's selected user."); } - emit selectUser(userID); + emit selectUser(userID, true); } catch (Exception const &e) { app().log().error(e.qwhat()); } @@ -242,15 +299,23 @@ void TrayIcon::setState(TrayIcon::State state, QString const &stateString, QStri } +//**************************************************************************************************************************************************** +/// \param[in] title The title. +/// \param[in] message The message. +//**************************************************************************************************************************************************** +void TrayIcon::showErrorPopupNotification(QString const &title, QString const &message) { +// this->showMessage(title, message, loadIconFromSVG(":/qml/icons/ic-exclamation-circle-filled.svg", errorColor)); + this->showMessage(title, message, loadIconFromSVG(":/qml/icons/ic-alert.svg")); +} + + //**************************************************************************************************************************************************** /// \param[in] svgPath The path of the SVG file for the icon. /// \param[in] color The color to apply to the icon. //**************************************************************************************************************************************************** void TrayIcon::generateStatusIcon(QString const &svgPath, QColor const &color) { // We use the SVG path as pixmap mask and fill it with the appropriate color - QString resourcePath = svgPath; - resourcePath.replace(QRegularExpression(R"(^\.\/)"), ":/qml/"); // QML resource path are a bit different from the Qt resources path. - QPixmap pixmap(resourcePath); + QPixmap pixmap(qmlResourcePathToQt(svgPath)); QPainter painter(&pixmap); painter.setCompositionMode(QPainter::CompositionMode_SourceIn); painter.fillRect(pixmap.rect(), color); @@ -259,9 +324,9 @@ void TrayIcon::generateStatusIcon(QString const &svgPath, QColor const &color) { } -//********************************************************************************************************************** +//**************************************************************************************************************************************************** // -//********************************************************************************************************************** +//**************************************************************************************************************************************************** void TrayIcon::refreshContextMenu() { if (!menu_) { app().log().error("Native tray icon context menu is null."); @@ -294,3 +359,5 @@ void TrayIcon::refreshContextMenu() { menu_->addSeparator(); menu_->addAction(tr("&Quit Bridge"), QKeySequence("Ctrl+Q"), &app().backend(), &QMLBackend::quit); } + + diff --git a/internal/frontend/bridge-gui/bridge-gui/TrayIcon.h b/internal/frontend/bridge-gui/bridge-gui/TrayIcon.h index 74cfdc94..326d0d7b 100644 --- a/internal/frontend/bridge-gui/bridge-gui/TrayIcon.h +++ b/internal/frontend/bridge-gui/bridge-gui/TrayIcon.h @@ -41,10 +41,10 @@ public: // data members TrayIcon& operator=(TrayIcon const&) = delete; ///< Disabled assignment operator. TrayIcon& operator=(TrayIcon&&) = delete; ///< Disabled move assignment operator. void setState(State state, QString const& stateString, QString const &statusIconPath); ///< Set the state of the icon - void showNotificationPopup(QString const& title, QString const &message, QString const& iconPath); ///< Display a pop up notification. + void showErrorPopupNotification(QString const& title, QString const &message); ///< Display a pop up notification. signals: - void selectUser(QString const& userID); ///< Signal for selecting a user with a given userID + void selectUser(QString const& userID, bool forceShowWindow); ///< Signal for selecting a user with a given userID private slots: void onMenuAboutToShow(); ///< Slot called before the context menu is shown. diff --git a/internal/frontend/bridge-gui/bridge-gui/qml/MainWindow.qml b/internal/frontend/bridge-gui/bridge-gui/qml/MainWindow.qml index 9295d7f5..3de94f99 100644 --- a/internal/frontend/bridge-gui/bridge-gui/qml/MainWindow.qml +++ b/internal/frontend/bridge-gui/bridge-gui/qml/MainWindow.qml @@ -95,9 +95,11 @@ ApplicationWindow { root.showAndRise() } - function onSelectUser(userID) { + function onSelectUser(userID, forceShowWindow) { contentWrapper.selectUser(userID) - root.showAndRise() + if (forceShowWindow) { + root.showAndRise() + } } } diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/User/User.cpp b/internal/frontend/bridge-gui/bridgepp/bridgepp/User/User.cpp index 041a4d12..0059f46a 100644 --- a/internal/frontend/bridge-gui/bridgepp/bridgepp/User/User.cpp +++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/User/User.cpp @@ -34,9 +34,7 @@ SPUser User::newUser(QObject *parent) { /// \param[in] parent The parent object. //**************************************************************************************************************************************************** User::User(QObject *parent) - : QObject(parent) - , imapFailureCooldownEndTime_(QDateTime::currentDateTime()) { - + : QObject(parent) { } @@ -355,22 +353,18 @@ QString User::stateToString(UserState state) { //**************************************************************************************************************************************************** -/// We display a notification and pop the application window if an IMAP client tries to connect to a signed out account, but we do not want to -/// do it repeatedly, as it's an intrusive action. This function let's you define a period of time during which the notification should not be -/// displayed. -/// -/// \param durationMSecs The duration of the period in milliseconds. +/// \param[in] durationMSecs The duration of the period in milliseconds. //**************************************************************************************************************************************************** -void User::startImapLoginFailureCooldown(qint64 durationMSecs) { - imapFailureCooldownEndTime_ = QDateTime::currentDateTime().addMSecs(durationMSecs); +void User::startNotificationCooldownPeriod(User::ENotification notification, qint64 durationMSecs) { + notificationCooldownList_[notification] = QDateTime::currentDateTime().addMSecs(durationMSecs); } //**************************************************************************************************************************************************** -/// \return true if we currently are in a cooldown period for the notification +/// \return true iff the notification is currently in a cooldown period. //**************************************************************************************************************************************************** -bool User::isInIMAPLoginFailureCooldown() const { - return QDateTime::currentDateTime() < imapFailureCooldownEndTime_; +bool User::isNotificationInCooldown(User::ENotification notification) const { + return notificationCooldownList_.contains(notification) && (QDateTime::currentDateTime() < notificationCooldownList_[notification]); } diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/User/User.h b/internal/frontend/bridge-gui/bridgepp/bridgepp/User/User.h index 78cfe8c9..d30c6b00 100644 --- a/internal/frontend/bridge-gui/bridgepp/bridgepp/User/User.h +++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/User/User.h @@ -62,6 +62,12 @@ typedef std::shared_ptr SPUser; ///< Type definition for shared poin class User : public QObject { Q_OBJECT +public: // data types + enum class ENotification { + IMAPLoginWhileSignedOut, ///< An IMAP client tried to login while the user is signed out. + IMAPPasswordFailure, ///< An IMAP client provided an invalid password for the user. + }; + public: // static member function static SPUser newUser(QObject *parent); ///< Create a new user static QString stateToString(UserState state); ///< Return a string describing a user state. @@ -74,8 +80,8 @@ public: // member functions. User &operator=(User &&) = delete; ///< Disabled move assignment operator. void update(User const &user); ///< Update the user. Q_INVOKABLE QString primaryEmailOrUsername() const; ///< Return the user primary email, or, if unknown its username. - void startImapLoginFailureCooldown(qint64 durationMSecs); ///< Start the user cooldown period for the IMAP login attempt while signed-out notification. - bool isInIMAPLoginFailureCooldown() const; ///< Check if the user in a IMAP login failure notification. + void startNotificationCooldownPeriod(ENotification notification, qint64 durationMSecs); ///< Start the user cooldown period for a notification. + bool isNotificationInCooldown(ENotification notification) const; ///< Return true iff the notification is in a cooldown period. public slots: // slots for QML generated calls @@ -147,7 +153,7 @@ private: // member functions. User(QObject *parent); ///< Default constructor. private: // data members. - QDateTime imapFailureCooldownEndTime_; ///< The end date/time for the IMAP login failure notification cooldown period. + QMap notificationCooldownList_; ///< A list of cooldown period end time for notifications. QString id_; ///< The userID. QString username_; ///< The username QString password_; ///< The IMAP password of the user. From 333daa05c5e8b3876694fd74cb3964c7894e5918 Mon Sep 17 00:00:00 2001 From: Xavier Michelon Date: Wed, 3 May 2023 15:19:02 +0200 Subject: [PATCH 03/43] feat(GODT-2540): pop-up notification error icon is loaded on startup. --- internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp | 8 ++++---- internal/frontend/bridge-gui/bridge-gui/TrayIcon.h | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp b/internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp index ccb3528b..bf8504ba 100644 --- a/internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp +++ b/internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp @@ -159,8 +159,9 @@ QString qmlResourcePathToQt(QString const &path) { //**************************************************************************************************************************************************** TrayIcon::TrayIcon() : QSystemTrayIcon() - , menu_(new QMenu) { - + , menu_(new QMenu) + , notificationErrorIcon_(loadIconFromSVG(":/qml/icons/ic-alert.svg")) + { this->generateDotIcons(); this->setContextMenu(menu_.get()); @@ -304,8 +305,7 @@ void TrayIcon::setState(TrayIcon::State state, QString const &stateString, QStri /// \param[in] message The message. //**************************************************************************************************************************************************** void TrayIcon::showErrorPopupNotification(QString const &title, QString const &message) { -// this->showMessage(title, message, loadIconFromSVG(":/qml/icons/ic-exclamation-circle-filled.svg", errorColor)); - this->showMessage(title, message, loadIconFromSVG(":/qml/icons/ic-alert.svg")); + this->showMessage(title, message, notificationErrorIcon_); } diff --git a/internal/frontend/bridge-gui/bridge-gui/TrayIcon.h b/internal/frontend/bridge-gui/bridge-gui/TrayIcon.h index 326d0d7b..058ca1c2 100644 --- a/internal/frontend/bridge-gui/bridge-gui/TrayIcon.h +++ b/internal/frontend/bridge-gui/bridge-gui/TrayIcon.h @@ -67,6 +67,7 @@ private: // data members QIcon greenDot_; ///< The green dot icon. QIcon greyDot_; ///< The grey dot icon. QIcon orangeDot_; ///< The orange dot icon. + QIcon const notificationErrorIcon_; ///< The error icon used for notifications. QTimer iconRefreshTimer_; ///< The timer used to periodically refresh the icon when DPI changes. QDateTime iconRefreshDeadline_; ///< The deadline for refreshing the icon From fdae8cb72901546a8a0bebd2d49a1b0a970cbc29 Mon Sep 17 00:00:00 2001 From: Xavier Michelon Date: Wed, 3 May 2023 16:40:29 +0200 Subject: [PATCH 04/43] feat(GODT-2540): make icon loading failure behavior consistent. --- .../bridge-gui/bridge-gui/TrayIcon.cpp | 58 +++++++++++-------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp b/internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp index bf8504ba..c4c616da 100644 --- a/internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp +++ b/internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp @@ -43,24 +43,23 @@ qint64 const iconRefreshDurationSecs = 10; ///< The total number of seconds duri QIcon loadIconFromImage(QString const &path) { QPixmap const pixmap(path); if (pixmap.isNull()) { - throw Exception(QString("Could create icon from image '%1'.").arg(path)); + throw Exception(QString("Could not create an icon from an image '%1'.").arg(path)); } return QIcon(pixmap); } //**************************************************************************************************************************************************** -/// \brief Load a multi-resolution icon from a SVG file. The image is assumed to be square. SVG is rasterized in 256, 128, 64, 32 and 16px. +/// \brief Generate an icon from a SVG renderer (a.k.a. path). /// -/// Note: QPixmap can load SVG files directly, but our SVG file are defined in small shape size and QPixmap will rasterize them a very low resolution -/// by default (eg. 16x16), which is insufficient for some uses. As a consequence, we manually generate a multi-resolution icon that render smoothly -/// at any acceptable resolution for an icon. -/// -/// \param[in] path The path of the SVG file. +/// \param[in] renderer The SVG renderer. +/// \param[in] color The color to use in case the SVG path is to be used as a mask. /// \return The icon. //**************************************************************************************************************************************************** -QIcon loadIconFromSVG(QString const &path, QColor const &color = QColor()) { - QSvgRenderer renderer(path); +QIcon loadIconFromSVGRenderer(QSvgRenderer &renderer, QColor const &color = QColor()) { + if (!renderer.isValid()) { + return QIcon(); + } QIcon icon; qint32 size = 256; @@ -82,6 +81,26 @@ QIcon loadIconFromSVG(QString const &path, QColor const &color = QColor()) { } +//**************************************************************************************************************************************************** +/// \brief Load a multi-resolution icon from a SVG file. The image is assumed to be square. SVG is rasterized in 256, 128, 64, 32 and 16px. +/// +/// Note: QPixmap can load SVG files directly, but our SVG file are defined in small shape size and QPixmap will rasterize them a very low resolution +/// by default (eg. 16x16), which is insufficient for some uses. As a consequence, we manually generate a multi-resolution icon that render smoothly +/// at any acceptable resolution for an icon. +/// +/// \param[in] path The path of the SVG file. +/// \return The icon. +//**************************************************************************************************************************************************** +QIcon loadIconFromSVG(QString const &path, QColor const &color = QColor()) { + QSvgRenderer renderer(path); + QIcon const icon = loadIconFromSVGRenderer(renderer, color); + if (icon.isNull()) { + Exception(QString("Could not create an icon from a vector image '%1'.").arg(path)); + } + return icon; +} + + //**************************************************************************************************************************************************** // //**************************************************************************************************************************************************** @@ -171,7 +190,6 @@ TrayIcon::TrayIcon() // some OSes/Desktop managers will automatically show main window when clicked, but not all, so we do it manually. connect(this, &TrayIcon::messageClicked, &app().backend(), &QMLBackend::showMainWindow); this->show(); - this->setState(State::Normal, QString(), QString()); // TrayIcon does not expose its screen, so we connect relevant screen events to our DPI change handler. for (QScreen *screen: QGuiApplication::screens()) { @@ -270,18 +288,17 @@ void TrayIcon::onIconRefreshTimer() { // //**************************************************************************************************************************************************** void TrayIcon::generateDotIcons() { - QPixmap dotSVG(":/qml/icons/ic-dot.svg"); + QSvgRenderer dotSVG(QString(":/qml/icons/ic-dot.svg")); + struct IconColor { QIcon &icon; QColor color; }; for (auto pair: QList {{ greenDot_, normalColor }, { greyDot_, greyColor }, { orangeDot_, warnColor }}) { - QPixmap p = dotSVG; - QPainter painter(&p); - painter.setCompositionMode(QPainter::CompositionMode_SourceIn); - painter.fillRect(p.rect(), pair.color); - painter.end(); - pair.icon = QIcon(p); + pair.icon = loadIconFromSVGRenderer(dotSVG, pair.color); + if (pair.icon.isNull()) { + throw Exception("Could not generate dot icon from vector file."); + } } } @@ -315,12 +332,7 @@ void TrayIcon::showErrorPopupNotification(QString const &title, QString const &m //**************************************************************************************************************************************************** void TrayIcon::generateStatusIcon(QString const &svgPath, QColor const &color) { // We use the SVG path as pixmap mask and fill it with the appropriate color - QPixmap pixmap(qmlResourcePathToQt(svgPath)); - QPainter painter(&pixmap); - painter.setCompositionMode(QPainter::CompositionMode_SourceIn); - painter.fillRect(pixmap.rect(), color); - painter.end(); - statusIcon_ = QIcon(pixmap); + statusIcon_ = loadIconFromSVG(qmlResourcePathToQt(svgPath), color); } From bd473030740c73b0b83fede5210e27dfdda1cdbb Mon Sep 17 00:00:00 2001 From: Xavier Michelon Date: Thu, 4 May 2023 11:48:05 +0200 Subject: [PATCH 05/43] feat(GODT-2611): bridge CLI exits on the first SIGINT / Ctrl+C. --- internal/frontend/cli/frontend.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/frontend/cli/frontend.go b/internal/frontend/cli/frontend.go index 4422e427..03d66e16 100644 --- a/internal/frontend/cli/frontend.go +++ b/internal/frontend/cli/frontend.go @@ -20,6 +20,7 @@ package cli import ( "errors" + "os" "github.com/ProtonMail/gluon/async" "github.com/ProtonMail/proton-bridge/v3/internal/bridge" @@ -60,6 +61,11 @@ func New( panicHandler: panicHandler, } + // We want to exit at the first Ctrl+C. By default, ishell requires two. + fe.Interrupt(func(_ *ishell.Context, _ int, _ string) { + os.Exit(1) + }) + // Clear commands. clearCmd := &ishell.Cmd{ Name: "clear", From b51d85e7682d149b34843e87d7d0b0f9d7808bf6 Mon Sep 17 00:00:00 2001 From: Xavier Michelon Date: Thu, 4 May 2023 21:24:17 +0200 Subject: [PATCH 06/43] chore: upgraded golangci-lint v1.52.2 and fixed all issues. --- .golangci.yml | 8 ++- Makefile | 2 +- internal/bridge/files.go | 6 +- internal/bridge/mocks.go | 4 +- internal/bridge/smtp_backend.go | 2 +- internal/bridge/user_test.go | 2 +- internal/certs/cert_store_darwin.go | 5 +- internal/certs/cert_store_darwin_test.go | 2 +- internal/frontend/cli/accounts.go | 4 +- internal/frontend/cli/system.go | 18 +++--- internal/frontend/cli/updates.go | 10 +-- internal/frontend/grpc/service_methods.go | 76 +++++++++++------------ internal/frontend/grpc/service_user.go | 14 ++--- internal/sentry/reporter.go | 2 +- internal/updater/install_darwin.go | 2 +- internal/user/events.go | 10 +-- internal/user/imap.go | 10 +-- internal/user/smtp_default.go | 2 +- internal/user/sync.go | 8 +-- internal/vault/types_file.go | 6 +- internal/versioner/remove_darwin.go | 2 +- pkg/message/build.go | 24 ++----- pkg/message/parser_test.go | 4 +- tests/ctx_bridge_test.go | 6 +- tests/fast.go | 2 +- tests/imap_test.go | 2 +- 26 files changed, 105 insertions(+), 128 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 6149b688..b44fe6d5 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -48,16 +48,13 @@ linters: disable-all: true enable: - - deadcode # Finds unused code [fast: true, auto-fix: false] - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: true, auto-fix: false] - gosimple # Linter for Go source code that specializes in simplifying a code [fast: true, auto-fix: false] - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: true, auto-fix: false] - ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false] - staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: true, auto-fix: false] - - structcheck # Finds unused struct fields [fast: true, auto-fix: false] - typecheck # Like the front-end of a Go compiler, parses and type-checks Go code [fast: true, auto-fix: false] - unused # Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false] - - varcheck # Finds unused global variables and constants [fast: true, auto-fix: false] - bodyclose # checks whether HTTP response body is closed successfully [fast: true, auto-fix: false] - depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false] - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false] @@ -119,3 +116,8 @@ linters: # - testpackage # linter that makes you use a separate _test package [fast: true, auto-fix: false] # - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers [fast: false, auto-fix: false] # - wrapcheck # Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false] + + # Deprecated: + # - structcheck # Finds unused struct fields [fast: true, auto-fix: false] deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused. + # - deadcode # Finds unused code [fast: true, auto-fix: false] deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused. + # - varcheck # Finds unused global variables and constants [fast: true, auto-fix: false] deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused. \ No newline at end of file diff --git a/Makefile b/Makefile index 4fbd1e79..39ffc991 100644 --- a/Makefile +++ b/Makefile @@ -183,7 +183,7 @@ ${RESOURCE_FILE}: ./dist/info.rc ./dist/${SRC_ICO} .FORCE ## Dev dependencies .PHONY: install-devel-tools install-linter install-go-mod-outdated install-git-hooks -LINTVER:="v1.50.0" +LINTVER:="v1.52.2" LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh" install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated diff --git a/internal/bridge/files.go b/internal/bridge/files.go index 475b1925..cb6a8cfc 100644 --- a/internal/bridge/files.go +++ b/internal/bridge/files.go @@ -58,11 +58,7 @@ func moveFile(from, to string) error { return err } - if err := os.Rename(from, to); err != nil { - return err - } - - return nil + return os.Rename(from, to) } func copyDir(from, to string) error { diff --git a/internal/bridge/mocks.go b/internal/bridge/mocks.go index 1c7bf934..63b16b8f 100644 --- a/internal/bridge/mocks.go +++ b/internal/bridge/mocks.go @@ -144,13 +144,13 @@ func (testUpdater *TestUpdater) SetLatestVersion(version, minAuto *semver.Versio } } -func (testUpdater *TestUpdater) GetVersionInfo(ctx context.Context, downloader updater.Downloader, channel updater.Channel) (updater.VersionInfo, error) { +func (testUpdater *TestUpdater) GetVersionInfo(_ context.Context, _ updater.Downloader, _ updater.Channel) (updater.VersionInfo, error) { testUpdater.lock.RLock() defer testUpdater.lock.RUnlock() return testUpdater.latest, nil } -func (testUpdater *TestUpdater) InstallUpdate(ctx context.Context, downloader updater.Downloader, update updater.VersionInfo) error { +func (testUpdater *TestUpdater) InstallUpdate(_ context.Context, _ updater.Downloader, _ updater.VersionInfo) error { return nil } diff --git a/internal/bridge/smtp_backend.go b/internal/bridge/smtp_backend.go index d48f3923..014edd81 100644 --- a/internal/bridge/smtp_backend.go +++ b/internal/bridge/smtp_backend.go @@ -72,7 +72,7 @@ func (s *smtpSession) Logout() error { return nil } -func (s *smtpSession) Mail(from string, opts *smtp.MailOptions) error { +func (s *smtpSession) Mail(from string, _ *smtp.MailOptions) error { s.from = from return nil } diff --git a/internal/bridge/user_test.go b/internal/bridge/user_test.go index 417e7bd7..135c8f23 100644 --- a/internal/bridge/user_test.go +++ b/internal/bridge/user_test.go @@ -709,6 +709,6 @@ func TestBridge_User_Refresh(t *testing.T) { } // getErr returns the error that was passed to it. -func getErr[T any](val T, err error) error { +func getErr[T any](_ T, err error) error { return err } diff --git a/internal/certs/cert_store_darwin.go b/internal/certs/cert_store_darwin.go index 8e1f9570..61a5977d 100644 --- a/internal/certs/cert_store_darwin.go +++ b/internal/certs/cert_store_darwin.go @@ -107,7 +107,6 @@ const ( // certPEMToDER converts a certificate in PEM format to DER format, which is the format required by Apple's Security framework. func certPEMToDER(certPEM []byte) ([]byte, error) { - block, left := pem.Decode(certPEM) if block == nil { return []byte{}, errors.New("invalid PEM certificate") @@ -127,7 +126,7 @@ func installCert(certPEM []byte) error { } p := C.CBytes(certDER) - defer C.free(unsafe.Pointer(p)) + defer C.free(unsafe.Pointer(p)) //nolint:unconvert errCode := C.installTrustedCert((*C.char)(p), (C.ulonglong)(len(certDER))) switch errCode { @@ -147,7 +146,7 @@ func uninstallCert(certPEM []byte) error { } p := C.CBytes(certDER) - defer C.free(unsafe.Pointer(p)) + defer C.free(unsafe.Pointer(p)) //nolint:unconvert if errCode := C.removeTrustedCert((*C.char)(p), (C.ulonglong)(len(certDER))); errCode != 0 { return fmt.Errorf("could not install certificate from keychain (error %v)", errCode) diff --git a/internal/certs/cert_store_darwin_test.go b/internal/certs/cert_store_darwin_test.go index 9ac9c698..3b1d419f 100644 --- a/internal/certs/cert_store_darwin_test.go +++ b/internal/certs/cert_store_darwin_test.go @@ -26,7 +26,7 @@ import ( ) // This test implies human interactions to enter password and is disabled by default. -func _TestTrustedCertsDarwin(t *testing.T) { +func _TestTrustedCertsDarwin(t *testing.T) { //nolint:unused template, err := NewTLSTemplate() require.NoError(t, err) diff --git a/internal/frontend/cli/accounts.go b/internal/frontend/cli/accounts.go index 09e3c15a..fdef4b17 100644 --- a/internal/frontend/cli/accounts.go +++ b/internal/frontend/cli/accounts.go @@ -305,11 +305,11 @@ func (f *frontendCLI) configureAppleMail(c *ishell.Context) { f.Printf("Apple Mail configured for %v with address %v\n", user.Username, user.Addresses[0]) } -func (f *frontendCLI) badEventSynchronize(c *ishell.Context) { +func (f *frontendCLI) badEventSynchronize(_ *ishell.Context) { f.badEventFeedback(true) } -func (f *frontendCLI) badEventLogout(c *ishell.Context) { +func (f *frontendCLI) badEventLogout(_ *ishell.Context) { f.badEventFeedback(false) } diff --git a/internal/frontend/cli/system.go b/internal/frontend/cli/system.go index 1290d0fa..3ea41dca 100644 --- a/internal/frontend/cli/system.go +++ b/internal/frontend/cli/system.go @@ -31,7 +31,7 @@ import ( "github.com/abiosoft/ishell" ) -func (f *frontendCLI) printLogDir(c *ishell.Context) { +func (f *frontendCLI) printLogDir(_ *ishell.Context) { if path, err := f.bridge.GetLogsPath(); err != nil { f.Println("Failed to determine location of log files") } else { @@ -39,17 +39,17 @@ func (f *frontendCLI) printLogDir(c *ishell.Context) { } } -func (f *frontendCLI) printManual(c *ishell.Context) { +func (f *frontendCLI) printManual(_ *ishell.Context) { f.Println("More instructions about the Bridge can be found at\n\n https://proton.me/mail/bridge") } -func (f *frontendCLI) printCredits(c *ishell.Context) { +func (f *frontendCLI) printCredits(_ *ishell.Context) { for _, pkg := range strings.Split(bridge.Credits, ";") { f.Println(pkg) } } -func (f *frontendCLI) changeIMAPSecurity(c *ishell.Context) { +func (f *frontendCLI) changeIMAPSecurity(_ *ishell.Context) { f.ShowPrompt(false) defer f.ShowPrompt(true) @@ -68,7 +68,7 @@ func (f *frontendCLI) changeIMAPSecurity(c *ishell.Context) { } } -func (f *frontendCLI) changeSMTPSecurity(c *ishell.Context) { +func (f *frontendCLI) changeSMTPSecurity(_ *ishell.Context) { f.ShowPrompt(false) defer f.ShowPrompt(true) @@ -131,7 +131,7 @@ func (f *frontendCLI) changeSMTPPort(c *ishell.Context) { } } -func (f *frontendCLI) allowProxy(c *ishell.Context) { +func (f *frontendCLI) allowProxy(_ *ishell.Context) { if f.bridge.GetProxyAllowed() { f.Println("Bridge is already set to use alternative routing to connect to Proton if it is being blocked.") return @@ -147,7 +147,7 @@ func (f *frontendCLI) allowProxy(c *ishell.Context) { } } -func (f *frontendCLI) disallowProxy(c *ishell.Context) { +func (f *frontendCLI) disallowProxy(_ *ishell.Context) { if !f.bridge.GetProxyAllowed() { f.Println("Bridge is already set to NOT use alternative routing to connect to Proton if it is being blocked.") return @@ -163,7 +163,7 @@ func (f *frontendCLI) disallowProxy(c *ishell.Context) { } } -func (f *frontendCLI) hideAllMail(c *ishell.Context) { +func (f *frontendCLI) hideAllMail(_ *ishell.Context) { if !f.bridge.GetShowAllMail() { f.Println("All Mail folder is not listed in your local client.") return @@ -179,7 +179,7 @@ func (f *frontendCLI) hideAllMail(c *ishell.Context) { } } -func (f *frontendCLI) showAllMail(c *ishell.Context) { +func (f *frontendCLI) showAllMail(_ *ishell.Context) { if f.bridge.GetShowAllMail() { f.Println("All Mail folder is listed in your local client.") return diff --git a/internal/frontend/cli/updates.go b/internal/frontend/cli/updates.go index baf253c8..2952b56e 100644 --- a/internal/frontend/cli/updates.go +++ b/internal/frontend/cli/updates.go @@ -23,7 +23,7 @@ import ( "github.com/abiosoft/ishell" ) -func (f *frontendCLI) checkUpdates(c *ishell.Context) { +func (f *frontendCLI) checkUpdates(_ *ishell.Context) { updateCh, done := f.bridge.GetEvents(events.UpdateAvailable{}, events.UpdateNotAvailable{}) defer done() @@ -38,7 +38,7 @@ func (f *frontendCLI) checkUpdates(c *ishell.Context) { } } -func (f *frontendCLI) enableAutoUpdates(c *ishell.Context) { +func (f *frontendCLI) enableAutoUpdates(_ *ishell.Context) { if f.bridge.GetAutoUpdate() { f.Println("Bridge is already set to automatically install updates.") return @@ -54,7 +54,7 @@ func (f *frontendCLI) enableAutoUpdates(c *ishell.Context) { } } -func (f *frontendCLI) disableAutoUpdates(c *ishell.Context) { +func (f *frontendCLI) disableAutoUpdates(_ *ishell.Context) { if !f.bridge.GetAutoUpdate() { f.Println("Bridge is already set to NOT automatically install updates.") return @@ -70,7 +70,7 @@ func (f *frontendCLI) disableAutoUpdates(c *ishell.Context) { } } -func (f *frontendCLI) selectEarlyChannel(c *ishell.Context) { +func (f *frontendCLI) selectEarlyChannel(_ *ishell.Context) { if f.bridge.GetUpdateChannel() == updater.EarlyChannel { f.Println("Bridge is already on the early-access update channel.") return @@ -86,7 +86,7 @@ func (f *frontendCLI) selectEarlyChannel(c *ishell.Context) { } } -func (f *frontendCLI) selectStableChannel(c *ishell.Context) { +func (f *frontendCLI) selectStableChannel(_ *ishell.Context) { if f.bridge.GetUpdateChannel() == updater.StableChannel { f.Println("Bridge is already on the stable update channel.") return diff --git a/internal/frontend/grpc/service_methods.go b/internal/frontend/grpc/service_methods.go index a49e698a..49e86b1f 100644 --- a/internal/frontend/grpc/service_methods.go +++ b/internal/frontend/grpc/service_methods.go @@ -47,7 +47,7 @@ import ( ) // CheckTokens implements the CheckToken gRPC service call. -func (s *Service) CheckTokens(ctx context.Context, clientConfigPath *wrapperspb.StringValue) (*wrapperspb.StringValue, error) { +func (s *Service) CheckTokens(_ context.Context, clientConfigPath *wrapperspb.StringValue) (*wrapperspb.StringValue, error) { s.log.Debug("CheckTokens") path := clientConfigPath.Value @@ -65,7 +65,7 @@ func (s *Service) CheckTokens(ctx context.Context, clientConfigPath *wrapperspb. return &wrapperspb.StringValue{Value: clientConfig.Token}, nil } -func (s *Service) AddLogEntry(ctx context.Context, request *AddLogEntryRequest) (*emptypb.Empty, error) { +func (s *Service) AddLogEntry(_ context.Context, request *AddLogEntryRequest) (*emptypb.Empty, error) { entry := s.log if len(request.Package) > 0 { @@ -93,7 +93,7 @@ func (s *Service) AddLogEntry(ctx context.Context, request *AddLogEntryRequest) } // GuiReady implement the GuiReady gRPC service call. -func (s *Service) GuiReady(ctx context.Context, _ *emptypb.Empty) (*GuiReadyResponse, error) { +func (s *Service) GuiReady(_ context.Context, _ *emptypb.Empty) (*GuiReadyResponse, error) { s.log.Debug("GuiReady") s.initializationDone.Do(s.initializing.Done) @@ -107,7 +107,7 @@ func (s *Service) GuiReady(ctx context.Context, _ *emptypb.Empty) (*GuiReadyResp } // Quit implement the Quit gRPC service call. -func (s *Service) Quit(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { +func (s *Service) Quit(_ context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) { s.log.Debug("Quit") return &emptypb.Empty{}, s.quit() } @@ -143,13 +143,13 @@ func (s *Service) Restart(ctx context.Context, empty *emptypb.Empty) (*emptypb.E return s.Quit(ctx, empty) } -func (s *Service) ShowOnStartup(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) { +func (s *Service) ShowOnStartup(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) { s.log.Debug("ShowOnStartup") return wrapperspb.Bool(s.showOnStartup), nil } -func (s *Service) SetIsAutostartOn(ctx context.Context, isOn *wrapperspb.BoolValue) (*emptypb.Empty, error) { +func (s *Service) SetIsAutostartOn(_ context.Context, isOn *wrapperspb.BoolValue) (*emptypb.Empty, error) { s.log.WithField("show", isOn.Value).Debug("SetIsAutostartOn") defer func() { _ = s.SendEvent(NewToggleAutostartFinishedEvent()) }() @@ -169,13 +169,13 @@ func (s *Service) SetIsAutostartOn(ctx context.Context, isOn *wrapperspb.BoolVal return &emptypb.Empty{}, nil } -func (s *Service) IsAutostartOn(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) { +func (s *Service) IsAutostartOn(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) { s.log.Debug("IsAutostartOn") return wrapperspb.Bool(s.bridge.GetAutostart()), nil } -func (s *Service) SetIsBetaEnabled(ctx context.Context, isEnabled *wrapperspb.BoolValue) (*emptypb.Empty, error) { +func (s *Service) SetIsBetaEnabled(_ context.Context, isEnabled *wrapperspb.BoolValue) (*emptypb.Empty, error) { s.log.WithField("isEnabled", isEnabled.Value).Debug("SetIsBetaEnabled") channel := updater.StableChannel @@ -191,13 +191,13 @@ func (s *Service) SetIsBetaEnabled(ctx context.Context, isEnabled *wrapperspb.Bo return &emptypb.Empty{}, nil } -func (s *Service) IsBetaEnabled(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) { +func (s *Service) IsBetaEnabled(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) { s.log.Debug("IsBetaEnabled") return wrapperspb.Bool(s.bridge.GetUpdateChannel() == updater.EarlyChannel), nil } -func (s *Service) SetIsAllMailVisible(ctx context.Context, isVisible *wrapperspb.BoolValue) (*emptypb.Empty, error) { +func (s *Service) SetIsAllMailVisible(_ context.Context, isVisible *wrapperspb.BoolValue) (*emptypb.Empty, error) { s.log.WithField("isVisible", isVisible.Value).Debug("SetIsAllMailVisible") if err := s.bridge.SetShowAllMail(isVisible.Value); err != nil { @@ -208,7 +208,7 @@ func (s *Service) SetIsAllMailVisible(ctx context.Context, isVisible *wrapperspb return &emptypb.Empty{}, nil } -func (s *Service) IsAllMailVisible(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) { +func (s *Service) IsAllMailVisible(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) { s.log.Debug("IsAllMailVisible") return wrapperspb.Bool(s.bridge.GetShowAllMail()), nil @@ -231,13 +231,13 @@ func (s *Service) IsTelemetryDisabled(_ context.Context, _ *emptypb.Empty) (*wra return wrapperspb.Bool(s.bridge.GetTelemetryDisabled()), nil } -func (s *Service) GoOs(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { +func (s *Service) GoOs(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { s.log.Debug("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(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) { +func (s *Service) TriggerReset(_ context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) { s.log.Debug("TriggerReset") go func() { @@ -248,13 +248,13 @@ func (s *Service) TriggerReset(ctx context.Context, _ *emptypb.Empty) (*emptypb. return &emptypb.Empty{}, nil } -func (s *Service) Version(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { +func (s *Service) Version(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { s.log.Debug("Version") return wrapperspb.String(s.bridge.GetCurrentVersion().Original()), nil } -func (s *Service) LogsPath(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { +func (s *Service) LogsPath(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { s.log.Debug("LogsPath") path, err := s.bridge.GetLogsPath() @@ -265,7 +265,7 @@ func (s *Service) LogsPath(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.S return wrapperspb.String(path), nil } -func (s *Service) LicensePath(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { +func (s *Service) LicensePath(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { s.log.Debug("LicensePath") return wrapperspb.String(s.bridge.GetLicenseFilePath()), nil @@ -275,7 +275,7 @@ func (s *Service) DependencyLicensesLink(_ context.Context, _ *emptypb.Empty) (* return wrapperspb.String(s.bridge.GetDependencyLicensesLink()), nil } -func (s *Service) ReleaseNotesPageLink(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { +func (s *Service) ReleaseNotesPageLink(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { s.latestLock.RLock() defer s.latestLock.RUnlock() @@ -289,7 +289,7 @@ func (s *Service) LandingPageLink(_ context.Context, _ *emptypb.Empty) (*wrapper return wrapperspb.String(s.latest.LandingPage), nil } -func (s *Service) SetColorSchemeName(ctx context.Context, name *wrapperspb.StringValue) (*emptypb.Empty, error) { +func (s *Service) SetColorSchemeName(_ context.Context, name *wrapperspb.StringValue) (*emptypb.Empty, error) { s.log.WithField("ColorSchemeName", name.Value).Debug("SetColorSchemeName") if !theme.IsAvailable(theme.Theme(name.Value)) { @@ -305,7 +305,7 @@ func (s *Service) SetColorSchemeName(ctx context.Context, name *wrapperspb.Strin return &emptypb.Empty{}, nil } -func (s *Service) ColorSchemeName(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { +func (s *Service) ColorSchemeName(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { s.log.Debug("ColorSchemeName") current := s.bridge.GetColorScheme() @@ -320,13 +320,13 @@ func (s *Service) ColorSchemeName(ctx context.Context, _ *emptypb.Empty) (*wrapp return wrapperspb.String(current), nil } -func (s *Service) CurrentEmailClient(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { +func (s *Service) CurrentEmailClient(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { s.log.Debug("CurrentEmailClient") return wrapperspb.String(s.bridge.GetCurrentUserAgent()), nil } -func (s *Service) ReportBug(ctx context.Context, report *ReportBugRequest) (*emptypb.Empty, error) { +func (s *Service) ReportBug(_ context.Context, report *ReportBugRequest) (*emptypb.Empty, error) { s.log.WithFields(logrus.Fields{ "osType": report.OsType, "osVersion": report.OsVersion, @@ -382,7 +382,7 @@ func (s *Service) ExportTLSCertificates(_ context.Context, folderPath *wrappersp return &emptypb.Empty{}, nil } -func (s *Service) ForceLauncher(ctx context.Context, launcher *wrapperspb.StringValue) (*emptypb.Empty, error) { +func (s *Service) ForceLauncher(_ context.Context, launcher *wrapperspb.StringValue) (*emptypb.Empty, error) { s.log.WithField("launcher", launcher.Value).Debug("ForceLauncher") s.restarter.Override(launcher.Value) @@ -390,7 +390,7 @@ func (s *Service) ForceLauncher(ctx context.Context, launcher *wrapperspb.String return &emptypb.Empty{}, nil } -func (s *Service) SetMainExecutable(ctx context.Context, exe *wrapperspb.StringValue) (*emptypb.Empty, error) { +func (s *Service) SetMainExecutable(_ context.Context, exe *wrapperspb.StringValue) (*emptypb.Empty, error) { s.log.WithField("executable", exe.Value).Debug("SetMainExecutable") s.restarter.AddFlags("--wait", exe.Value) @@ -398,7 +398,7 @@ func (s *Service) SetMainExecutable(ctx context.Context, exe *wrapperspb.StringV return &emptypb.Empty{}, nil } -func (s *Service) Login(ctx context.Context, login *LoginRequest) (*emptypb.Empty, error) { +func (s *Service) Login(_ context.Context, login *LoginRequest) (*emptypb.Empty, error) { s.log.WithField("username", login.Username).Debug("Login") go func() { @@ -454,7 +454,7 @@ func (s *Service) Login(ctx context.Context, login *LoginRequest) (*emptypb.Empt return &emptypb.Empty{}, nil } -func (s *Service) Login2FA(ctx context.Context, login *LoginRequest) (*emptypb.Empty, error) { +func (s *Service) Login2FA(_ context.Context, login *LoginRequest) (*emptypb.Empty, error) { s.log.WithField("username", login.Username).Debug("Login2FA") go func() { @@ -499,7 +499,7 @@ func (s *Service) Login2FA(ctx context.Context, login *LoginRequest) (*emptypb.E return &emptypb.Empty{}, nil } -func (s *Service) Login2Passwords(ctx context.Context, login *LoginRequest) (*emptypb.Empty, error) { +func (s *Service) Login2Passwords(_ context.Context, login *LoginRequest) (*emptypb.Empty, error) { s.log.WithField("username", login.Username).Debug("Login2Passwords") go func() { @@ -521,7 +521,7 @@ func (s *Service) Login2Passwords(ctx context.Context, login *LoginRequest) (*em return &emptypb.Empty{}, nil } -func (s *Service) LoginAbort(ctx context.Context, loginAbort *LoginAbortRequest) (*emptypb.Empty, error) { +func (s *Service) LoginAbort(_ context.Context, loginAbort *LoginAbortRequest) (*emptypb.Empty, error) { s.log.WithField("username", loginAbort.Username).Debug("LoginAbort") go func() { @@ -565,7 +565,7 @@ func (s *Service) CheckUpdate(context.Context, *emptypb.Empty) (*emptypb.Empty, return &emptypb.Empty{}, nil } -func (s *Service) InstallUpdate(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) { +func (s *Service) InstallUpdate(_ context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) { s.log.Debug("InstallUpdate") go func() { @@ -579,7 +579,7 @@ func (s *Service) InstallUpdate(ctx context.Context, _ *emptypb.Empty) (*emptypb return &emptypb.Empty{}, nil } -func (s *Service) SetIsAutomaticUpdateOn(ctx context.Context, isOn *wrapperspb.BoolValue) (*emptypb.Empty, error) { +func (s *Service) SetIsAutomaticUpdateOn(_ context.Context, isOn *wrapperspb.BoolValue) (*emptypb.Empty, error) { s.log.WithField("isOn", isOn.Value).Debug("SetIsAutomaticUpdateOn") if currentlyOn := s.bridge.GetAutoUpdate(); currentlyOn == isOn.Value { @@ -594,19 +594,19 @@ func (s *Service) SetIsAutomaticUpdateOn(ctx context.Context, isOn *wrapperspb.B return &emptypb.Empty{}, nil } -func (s *Service) IsAutomaticUpdateOn(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) { +func (s *Service) IsAutomaticUpdateOn(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) { s.log.Debug("IsAutomaticUpdateOn") return wrapperspb.Bool(s.bridge.GetAutoUpdate()), nil } -func (s *Service) DiskCachePath(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { +func (s *Service) DiskCachePath(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { s.log.Debug("DiskCachePath") return wrapperspb.String(s.bridge.GetGluonCacheDir()), nil } -func (s *Service) SetDiskCachePath(ctx context.Context, newPath *wrapperspb.StringValue) (*emptypb.Empty, error) { +func (s *Service) SetDiskCachePath(_ context.Context, newPath *wrapperspb.StringValue) (*emptypb.Empty, error) { s.log.WithField("path", newPath.Value).Debug("setDiskCachePath") go func() { @@ -637,7 +637,7 @@ func (s *Service) SetDiskCachePath(ctx context.Context, newPath *wrapperspb.Stri return &emptypb.Empty{}, nil } -func (s *Service) SetIsDoHEnabled(ctx context.Context, isEnabled *wrapperspb.BoolValue) (*emptypb.Empty, error) { +func (s *Service) SetIsDoHEnabled(_ context.Context, isEnabled *wrapperspb.BoolValue) (*emptypb.Empty, error) { s.log.WithField("isEnabled", isEnabled.Value).Debug("SetIsDohEnabled") if err := s.bridge.SetProxyAllowed(isEnabled.Value); err != nil { @@ -648,7 +648,7 @@ func (s *Service) SetIsDoHEnabled(ctx context.Context, isEnabled *wrapperspb.Boo return &emptypb.Empty{}, nil } -func (s *Service) IsDoHEnabled(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) { +func (s *Service) IsDoHEnabled(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) { s.log.Debug("IsDohEnabled") return wrapperspb.Bool(s.bridge.GetProxyAllowed()), nil @@ -715,19 +715,19 @@ func (s *Service) SetMailServerSettings(_ context.Context, settings *ImapSmtpSet return &emptypb.Empty{}, nil } -func (s *Service) Hostname(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { +func (s *Service) Hostname(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { s.log.Debug("Hostname") return wrapperspb.String(constants.Host), nil } -func (s *Service) IsPortFree(ctx context.Context, port *wrapperspb.Int32Value) (*wrapperspb.BoolValue, error) { +func (s *Service) IsPortFree(_ context.Context, port *wrapperspb.Int32Value) (*wrapperspb.BoolValue, error) { s.log.Debug("IsPortFree") return wrapperspb.Bool(ports.IsPortFree(int(port.Value))), nil } -func (s *Service) AvailableKeychains(ctx context.Context, _ *emptypb.Empty) (*AvailableKeychainsResponse, error) { +func (s *Service) AvailableKeychains(_ context.Context, _ *emptypb.Empty) (*AvailableKeychainsResponse, error) { s.log.Debug("AvailableKeychains") return &AvailableKeychainsResponse{Keychains: maps.Keys(keychain.Helpers)}, nil @@ -757,7 +757,7 @@ func (s *Service) SetCurrentKeychain(ctx context.Context, keychain *wrapperspb.S return &emptypb.Empty{}, nil } -func (s *Service) CurrentKeychain(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { +func (s *Service) CurrentKeychain(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { s.log.Debug("CurrentKeychain") helper, err := s.bridge.GetKeychainApp() diff --git a/internal/frontend/grpc/service_user.go b/internal/frontend/grpc/service_user.go index 8a72c985..91473c60 100644 --- a/internal/frontend/grpc/service_user.go +++ b/internal/frontend/grpc/service_user.go @@ -28,7 +28,7 @@ import ( "google.golang.org/protobuf/types/known/wrapperspb" ) -func (s *Service) GetUserList(ctx context.Context, _ *emptypb.Empty) (*UserListResponse, error) { +func (s *Service) GetUserList(_ context.Context, _ *emptypb.Empty) (*UserListResponse, error) { s.log.Debug("GetUserList") userIDs := s.bridge.GetUserIDs() @@ -51,7 +51,7 @@ func (s *Service) GetUserList(ctx context.Context, _ *emptypb.Empty) (*UserListR return &UserListResponse{Users: userList}, nil } -func (s *Service) GetUser(ctx context.Context, userID *wrapperspb.StringValue) (*User, error) { +func (s *Service) GetUser(_ context.Context, userID *wrapperspb.StringValue) (*User, error) { s.log.WithField("userID", userID).Debug("GetUser") user, err := s.bridge.GetUserInfo(userID.Value) @@ -62,7 +62,7 @@ func (s *Service) GetUser(ctx context.Context, userID *wrapperspb.StringValue) ( return grpcUserFromInfo(user), nil } -func (s *Service) SetUserSplitMode(ctx context.Context, splitMode *UserSplitModeRequest) (*emptypb.Empty, error) { +func (s *Service) SetUserSplitMode(_ context.Context, splitMode *UserSplitModeRequest) (*emptypb.Empty, error) { s.log.WithField("UserID", splitMode.UserID).WithField("Active", splitMode.Active).Debug("SetUserSplitMode") user, err := s.bridge.GetUserInfo(splitMode.UserID) @@ -96,7 +96,7 @@ func (s *Service) SetUserSplitMode(ctx context.Context, splitMode *UserSplitMode return &emptypb.Empty{}, nil } -func (s *Service) SendBadEventUserFeedback(ctx context.Context, feedback *UserBadEventFeedbackRequest) (*emptypb.Empty, error) { +func (s *Service) SendBadEventUserFeedback(_ context.Context, feedback *UserBadEventFeedbackRequest) (*emptypb.Empty, error) { l := s.log.WithField("UserID", feedback.UserID).WithField("doResync", feedback.DoResync) l.Debug("SendBadEventUserFeedback") @@ -114,7 +114,7 @@ func (s *Service) SendBadEventUserFeedback(ctx context.Context, feedback *UserBa return &emptypb.Empty{}, nil } -func (s *Service) LogoutUser(ctx context.Context, userID *wrapperspb.StringValue) (*emptypb.Empty, error) { +func (s *Service) LogoutUser(_ context.Context, userID *wrapperspb.StringValue) (*emptypb.Empty, error) { s.log.WithField("UserID", userID.Value).Debug("LogoutUser") if _, err := s.bridge.GetUserInfo(userID.Value); err != nil { @@ -132,7 +132,7 @@ func (s *Service) LogoutUser(ctx context.Context, userID *wrapperspb.StringValue return &emptypb.Empty{}, nil } -func (s *Service) RemoveUser(ctx context.Context, userID *wrapperspb.StringValue) (*emptypb.Empty, error) { +func (s *Service) RemoveUser(_ context.Context, userID *wrapperspb.StringValue) (*emptypb.Empty, error) { s.log.WithField("UserID", userID.Value).Debug("RemoveUser") go func() { @@ -147,7 +147,7 @@ func (s *Service) RemoveUser(ctx context.Context, userID *wrapperspb.StringValue return &emptypb.Empty{}, nil } -func (s *Service) ConfigureUserAppleMail(ctx context.Context, request *ConfigureAppleMailRequest) (*emptypb.Empty, error) { +func (s *Service) ConfigureUserAppleMail(_ context.Context, request *ConfigureAppleMailRequest) (*emptypb.Empty, error) { s.log.WithField("UserID", request.UserID).WithField("Address", request.Address).Debug("ConfigureUserAppleMail") sslWasEnabled := s.bridge.GetSMTPSSL() diff --git a/internal/sentry/reporter.go b/internal/sentry/reporter.go index 1ceb084c..2b69b4ad 100644 --- a/internal/sentry/reporter.go +++ b/internal/sentry/reporter.go @@ -203,7 +203,7 @@ func SkipDuringUnwind() { } // EnhanceSentryEvent swaps type with value and removes panic handlers from the stacktrace. -func EnhanceSentryEvent(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { +func EnhanceSentryEvent(event *sentry.Event, _ *sentry.EventHint) *sentry.Event { for idx, exception := range event.Exception { exception.Type, exception.Value = exception.Value, exception.Type if exception.Stacktrace != nil { diff --git a/internal/updater/install_darwin.go b/internal/updater/install_darwin.go index 45e07b58..c55420ca 100644 --- a/internal/updater/install_darwin.go +++ b/internal/updater/install_darwin.go @@ -62,6 +62,6 @@ func (i *InstallerDarwin) InstallUpdate(_ *semver.Version, r io.Reader) error { return syncFolders(oldBundle, newBundle) } -func (i *InstallerDarwin) IsAlreadyInstalled(version *semver.Version) bool { +func (i *InstallerDarwin) IsAlreadyInstalled(_ *semver.Version) bool { return false } diff --git a/internal/user/events.go b/internal/user/events.go index 35ee921c..760d678d 100644 --- a/internal/user/events.go +++ b/internal/user/events.go @@ -394,7 +394,7 @@ func (user *User) handleLabelEvents(ctx context.Context, labelEvents []proton.La return nil } -func (user *User) handleCreateLabelEvent(ctx context.Context, event proton.LabelEvent) ([]imap.Update, error) { //nolint:unparam +func (user *User) handleCreateLabelEvent(_ context.Context, event proton.LabelEvent) ([]imap.Update, error) { //nolint:unparam return safe.LockRetErr(func() ([]imap.Update, error) { var updates []imap.Update @@ -480,7 +480,7 @@ func (user *User) handleUpdateLabelEvent(ctx context.Context, event proton.Label }, user.apiLabelsLock, user.updateChLock) } -func (user *User) handleDeleteLabelEvent(ctx context.Context, event proton.LabelEvent) ([]imap.Update, error) { //nolint:unparam +func (user *User) handleDeleteLabelEvent(_ context.Context, event proton.LabelEvent) ([]imap.Update, error) { //nolint:unparam return safe.LockRetErr(func() ([]imap.Update, error) { var updates []imap.Update @@ -643,7 +643,7 @@ func (user *User) handleCreateMessageEvent(ctx context.Context, message proton.M }, user.apiUserLock, user.apiAddrsLock, user.apiLabelsLock, user.updateChLock) } -func (user *User) handleUpdateMessageEvent(ctx context.Context, message proton.MessageMetadata) ([]imap.Update, error) { //nolint:unparam +func (user *User) handleUpdateMessageEvent(_ context.Context, message proton.MessageMetadata) ([]imap.Update, error) { //nolint:unparam return safe.RLockRetErr(func() ([]imap.Update, error) { user.log.WithFields(logrus.Fields{ "messageID": message.ID, @@ -680,7 +680,7 @@ func (user *User) handleUpdateMessageEvent(ctx context.Context, message proton.M }, user.apiLabelsLock, user.updateChLock) } -func (user *User) handleDeleteMessageEvent(ctx context.Context, event proton.MessageEvent) ([]imap.Update, error) { //nolint:unparam +func (user *User) handleDeleteMessageEvent(_ context.Context, event proton.MessageEvent) ([]imap.Update, error) { return safe.RLockRetErr(func() ([]imap.Update, error) { user.log.WithField("messageID", event.ID).Info("Handling message deleted event") @@ -696,7 +696,7 @@ func (user *User) handleDeleteMessageEvent(ctx context.Context, event proton.Mes }, user.updateChLock) } -func (user *User) handleUpdateDraftEvent(ctx context.Context, event proton.MessageEvent) ([]imap.Update, error) { //nolint:unparam +func (user *User) handleUpdateDraftEvent(ctx context.Context, event proton.MessageEvent) ([]imap.Update, error) { return safe.RLockRetErr(func() ([]imap.Update, error) { user.log.WithFields(logrus.Fields{ "messageID": event.ID, diff --git a/internal/user/imap.go b/internal/user/imap.go index 1c678495..97841a4d 100644 --- a/internal/user/imap.go +++ b/internal/user/imap.go @@ -270,7 +270,7 @@ func (conn *imapConnector) CreateMessage( mailboxID imap.MailboxID, literal []byte, flags imap.FlagSet, - date time.Time, + _ time.Time, ) (imap.Message, []byte, error) { defer conn.goPollAPIEvents(false) @@ -435,11 +435,11 @@ func (conn *imapConnector) MoveMessages(ctx context.Context, messageIDs []imap.M var result bool if v, ok := conn.apiLabels[string(labelFromID)]; ok && v.Type == proton.LabelTypeLabel { - result = result || true + result = true } if v, ok := conn.apiLabels[string(labelToID)]; ok && (v.Type == proton.LabelTypeFolder || v.Type == proton.LabelTypeSystem) { - result = result || true + result = true } return result @@ -505,7 +505,7 @@ func (conn *imapConnector) GetMailboxVisibility(_ context.Context, mailboxID ima } // Close the connector will no longer be used and all resources should be closed/released. -func (conn *imapConnector) Close(ctx context.Context) error { +func (conn *imapConnector) Close(_ context.Context) error { return nil } @@ -520,7 +520,7 @@ func (conn *imapConnector) importMessage( if err := safe.RLockRet(func() error { return withAddrKR(conn.apiUser, conn.apiAddrs[conn.addrID], conn.vault.KeyPass(), func(_, addrKR *crypto.KeyRing) error { - messageID := "" + var messageID string if slices.Contains(labelIDs, proton.DraftsLabel) { msg, err := conn.createDraft(ctx, literal, addrKR, conn.apiAddrs[conn.addrID]) diff --git a/internal/user/smtp_default.go b/internal/user/smtp_default.go index 0f048d10..11bb8ded 100644 --- a/internal/user/smtp_default.go +++ b/internal/user/smtp_default.go @@ -19,6 +19,6 @@ package user -func debugDumpToDisk(b []byte) error { +func debugDumpToDisk(_ []byte) error { return nil } diff --git a/internal/user/sync.go b/internal/user/sync.go index d3da5d8f..55012a32 100644 --- a/internal/user/sync.go +++ b/internal/user/sync.go @@ -572,10 +572,10 @@ func (user *User) syncMessages( // We could sync a placeholder message here, but for now we skip it entirely. continue - } else { - if err := vault.RemFailedMessageID(res.messageID); err != nil { - logrus.WithError(err).Error("Failed to remove failed message ID") - } + } + + if err := vault.RemFailedMessageID(res.messageID); err != nil { + logrus.WithError(err).Error("Failed to remove failed message ID") } targetInfo := addressToIndex[res.addressID] diff --git a/internal/vault/types_file.go b/internal/vault/types_file.go index 6781114d..99da52e8 100644 --- a/internal/vault/types_file.go +++ b/internal/vault/types_file.go @@ -48,11 +48,7 @@ func unmarshalFile[T any](gcm cipher.AEAD, b []byte, data *T) error { } } - if err := msgpack.Unmarshal(dec, data); err != nil { - return err - } - - return nil + return msgpack.Unmarshal(dec, data) } func marshalFile[T any](gcm cipher.AEAD, t T) ([]byte, error) { diff --git a/internal/versioner/remove_darwin.go b/internal/versioner/remove_darwin.go index 3ffe26c0..6bcc20e6 100644 --- a/internal/versioner/remove_darwin.go +++ b/internal/versioner/remove_darwin.go @@ -29,7 +29,7 @@ func (v *Versioner) RemoveOldVersions() error { } // RemoveOtherVersions removes all but the specific provided app version. -func (v *Versioner) RemoveOtherVersions(versionToKeep *semver.Version) error { +func (v *Versioner) RemoveOtherVersions(_ *semver.Version) error { // darwin does not use the versioner; removal is a noop. return nil } diff --git a/pkg/message/build.go b/pkg/message/build.go index 5e7e44bb..baf916ee 100644 --- a/pkg/message/build.go +++ b/pkg/message/build.go @@ -92,11 +92,7 @@ func buildSimpleRFC822(kr *crypto.KeyRing, msg proton.Message, opts JobOptions, return err } - if err := w.Close(); err != nil { - return err - } - - return nil + return w.Close() } func buildMultipartRFC822( @@ -148,11 +144,7 @@ func buildMultipartRFC822( } } - if err := w.Close(); err != nil { - return err - } - - return nil + return w.Close() } func writeTextPart( @@ -319,11 +311,7 @@ func buildPGPMIMEFallbackRFC822(msg proton.Message, opts JobOptions, buf *bytes. return err } - if err := w.Close(); err != nil { - return err - } - - return nil + return w.Close() } func writeMultipartSignedRFC822(header message.Header, body []byte, sig proton.Signature, buf *bytes.Buffer) error { @@ -379,11 +367,7 @@ func writeMultipartSignedRFC822(header message.Header, body []byte, sig proton.S return err } - if err := mw.Close(); err != nil { - return err - } - - return nil + return mw.Close() } func writeMultipartEncryptedRFC822(header message.Header, body []byte, buf *bytes.Buffer) error { diff --git a/pkg/message/parser_test.go b/pkg/message/parser_test.go index de28aed0..981c06a9 100644 --- a/pkg/message/parser_test.go +++ b/pkg/message/parser_test.go @@ -673,7 +673,7 @@ func TestParsePanic(t *testing.T) { require.Error(t, err) } -func TestParseTextPlainWithPdfttachmentCyrillic(t *testing.T) { +func TestParseTextPlainWithPdfAttachmentCyrillic(t *testing.T) { f := getFileReader("text_plain_pdf_attachment_cyrillic.eml") m, err := Parse(f) @@ -718,6 +718,6 @@ func getFileReader(filename string) io.Reader { type panicReader struct{} -func (panicReader) Read(p []byte) (int, error) { +func (panicReader) Read(_ []byte) (int, error) { panic("lol") } diff --git a/tests/ctx_bridge_test.go b/tests/ctx_bridge_test.go index 0afe93d7..5c62b0bf 100644 --- a/tests/ctx_bridge_test.go +++ b/tests/ctx_bridge_test.go @@ -351,8 +351,8 @@ func (t *testCtx) expectProxyCtlAllowProxy() { type mockRestarter struct{} -func (m *mockRestarter) Set(restart, crash bool) {} +func (m *mockRestarter) Set(_, _ bool) {} -func (m *mockRestarter) AddFlags(flags ...string) {} +func (m *mockRestarter) AddFlags(_ ...string) {} -func (m *mockRestarter) Override(exe string) {} +func (m *mockRestarter) Override(_ string) {} diff --git a/tests/fast.go b/tests/fast.go index 84befbeb..e08283a6 100644 --- a/tests/fast.go +++ b/tests/fast.go @@ -28,7 +28,7 @@ var ( preCompKeyPEM []byte ) -func FastGenerateCert(template *x509.Certificate) ([]byte, []byte, error) { +func FastGenerateCert(_ *x509.Certificate) ([]byte, []byte, error) { return preCompCertPEM, preCompKeyPEM, nil } diff --git a/tests/imap_test.go b/tests/imap_test.go index 155b6e37..ab17cddc 100644 --- a/tests/imap_test.go +++ b/tests/imap_test.go @@ -470,7 +470,7 @@ func (s *scenario) imapClientAppendsToMailbox(clientID string, file, mailbox str return nil } -func (s *scenario) imapClientsMoveMessageWithSubjectUserFromToByOrderedOperations(sourceIMAPClient, targetIMAPClient, messageSubject, bddUserID, targetMailboxName, op1, op2, op3 string) error { +func (s *scenario) imapClientsMoveMessageWithSubjectUserFromToByOrderedOperations(sourceIMAPClient, targetIMAPClient, messageSubject, _, targetMailboxName, op1, op2, op3 string) error { // call NOOP to prevent unilateral updates in following FETCH _, sourceClient := s.t.getIMAPClient(sourceIMAPClient) _, targetClient := s.t.getIMAPClient(targetIMAPClient) From 324593596ab2f55c390efae36f9828d1e47296ca Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Thu, 4 May 2023 13:55:46 +0200 Subject: [PATCH 07/43] chore: Update Gluon for async.Group.Do() fix https://github.com/ProtonMail/gluon/pull/346 --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index df84a0d3..fb5c2ea2 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 github.com/Masterminds/semver/v3 v3.2.0 - github.com/ProtonMail/gluon v0.16.1-0.20230428090920-2797a1764f16 + github.com/ProtonMail/gluon v0.16.1-0.20230504091128-d8a3371a4ba5 github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a github.com/ProtonMail/go-proton-api v0.4.1-0.20230426081144-f77778bae1be github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton diff --git a/go.sum b/go.sum index a9a76587..1194b086 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,8 @@ github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkF github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g= github.com/ProtonMail/gluon v0.16.1-0.20230428090920-2797a1764f16 h1:X5kb4PwVrgVDQjBkpiobYrDlqKDMuS1o92Ty+rZ1ptE= github.com/ProtonMail/gluon v0.16.1-0.20230428090920-2797a1764f16/go.mod h1:yA4hk6CJw0BMo+YL8Y3ckCYs5L20sysu9xseshwY3QI= +github.com/ProtonMail/gluon v0.16.1-0.20230504091128-d8a3371a4ba5 h1:plqDYC9wOaO/D/QJWMB1nwjjOkT3bIIiXrgounnaOhc= +github.com/ProtonMail/gluon v0.16.1-0.20230504091128-d8a3371a4ba5/go.mod h1:yA4hk6CJw0BMo+YL8Y3ckCYs5L20sysu9xseshwY3QI= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= From a0db1645f25320f3b42037cc1e21226901a297d5 Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Fri, 5 May 2023 15:23:24 +0200 Subject: [PATCH 08/43] fix(GODT-2614): Handle failed update during sync The sync process was getting stuck since we never handled the case where the update to Gluon failed. This caused the flush stage to exist, but the sync process would continue until it eventually gets stuck due to lack of progress. --- internal/bridge/sync_unix_test.go | 81 +++++++++++++++++++++++++++++++ internal/user/sync.go | 4 ++ 2 files changed, 85 insertions(+) create mode 100644 internal/bridge/sync_unix_test.go diff --git a/internal/bridge/sync_unix_test.go b/internal/bridge/sync_unix_test.go new file mode 100644 index 00000000..21b97924 --- /dev/null +++ b/internal/bridge/sync_unix_test.go @@ -0,0 +1,81 @@ +// Copyright (c) 2023 Proton AG +// +// This file is part of Proton Mail Bridge. +// +// Proton Mail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Proton Mail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Proton Mail Bridge. If not, see . + +//go:build !windows + +package bridge_test + +import ( + "context" + "syscall" + "testing" + + "github.com/ProtonMail/go-proton-api" + "github.com/ProtonMail/go-proton-api/server" + "github.com/ProtonMail/proton-bridge/v3/internal/bridge" + "github.com/ProtonMail/proton-bridge/v3/internal/events" + "github.com/stretchr/testify/require" +) + +func TestBridge_SyncExistsWithErrorWhenTooManyFilesAreOpen(t *testing.T) { + var rlimitCurrent syscall.Rlimit + + require.NoError(t, syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlimitCurrent)) + + // Restore RLimit for Process at the end of this test + defer func() { + require.NoError(t, syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlimitCurrent)) + }() + + rlimit := syscall.Rlimit{ + Max: 100, + Cur: 100, + } + + require.NoError(t, syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlimit)) + + numMsg := 1 << 8 + + withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) { + userID, addrID, err := s.CreateUser("imap", password) + require.NoError(t, err) + + labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder) + require.NoError(t, err) + + withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) { + createNumMessages(ctx, t, c, addrID, labelID, numMsg) + }) + + // The initial user should be fully synced. + withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) { + syncCh, done := bridge.GetEvents(events.SyncFailed{}) + defer done() + + userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil) + require.NoError(t, err) + + evt := <-syncCh + switch e := evt.(type) { + case events.SyncFailed: + require.Equal(t, userID, e.UserID) + default: + require.Fail(t, "Expected events.SyncFailed{}") + } + }) + }, server.WithTLS(false)) +} diff --git a/internal/user/sync.go b/internal/user/sync.go index 55012a32..87ba6ca1 100644 --- a/internal/user/sync.go +++ b/internal/user/sync.go @@ -610,6 +610,10 @@ func (user *User) syncMessages( }, logging.Labels{"sync-stage": "flush"}) for flushUpdate := range flushUpdateCh { + if flushUpdate.err != nil { + return flushUpdate.err + } + if err := vault.SetLastMessageID(flushUpdate.messageID); err != nil { return fmt.Errorf("failed to set last synced message ID: %w", err) } From 01aa19edffb8c9ce242a2e39be4cb518c6e0ecba Mon Sep 17 00:00:00 2001 From: Xavier Michelon Date: Mon, 8 May 2023 07:40:05 +0200 Subject: [PATCH 09/43] fix(GODT-2615): remove keyboard shortcut for tray icon context menu on Windows and Linux. --- .../bridge-gui/bridge-gui/TrayIcon.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp b/internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp index c4c616da..86ba6b0b 100644 --- a/internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp +++ b/internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp @@ -104,7 +104,7 @@ QIcon loadIconFromSVG(QString const &path, QColor const &color = QColor()) { //**************************************************************************************************************************************************** // //**************************************************************************************************************************************************** -QIcon loadIcon(QString const& path) { +QIcon loadIcon(QString const &path) { if (path.endsWith(".svg", Qt::CaseInsensitive)) { return loadIconFromSVG(path); } @@ -179,8 +179,7 @@ QString qmlResourcePathToQt(QString const &path) { TrayIcon::TrayIcon() : QSystemTrayIcon() , menu_(new QMenu) - , notificationErrorIcon_(loadIconFromSVG(":/qml/icons/ic-alert.svg")) - { + , notificationErrorIcon_(loadIconFromSVG(":/qml/icons/ic-alert.svg")) { this->generateDotIcons(); this->setContextMenu(menu_.get()); @@ -348,8 +347,10 @@ void TrayIcon::refreshContextMenu() { menu_->clear(); menu_->addAction(statusIcon_, stateString_, &app().backend(), &QMLBackend::showMainWindow); menu_->addSeparator(); + QKeySequence noShortcut; UserList const &users = app().backend().users(); qint32 const userCount = users.count(); + bool const onMac = onMacOS(); for (qint32 i = 0; i < userCount; i++) { User const &user = *users.get(i); UserState const state = user.state(); @@ -357,7 +358,7 @@ void TrayIcon::refreshContextMenu() { action->setIcon((UserState::Connected == state) ? greenDot_ : (UserState::Locked == state ? orangeDot_ : greyDot_)); action->setData(user.id()); connect(action, &QAction::triggered, this, &TrayIcon::onUserClicked); - if (i < 10) { + if ((i < 10) && onMac) { action->setShortcut(QKeySequence(QString("Ctrl+%1").arg((i + 1) % 10))); } menu_->addAction(action); @@ -365,11 +366,12 @@ void TrayIcon::refreshContextMenu() { if (userCount) { menu_->addSeparator(); } - menu_->addAction(tr("&Open Bridge"), QKeySequence("Ctrl+O"), &app().backend(), &QMLBackend::showMainWindow); - menu_->addAction(tr("&Help"), QKeySequence("Ctrl+F1"), &app().backend(), &QMLBackend::showHelp); - menu_->addAction(tr("&Settings"), QKeySequence("Ctrl+,"), &app().backend(), &QMLBackend::showSettings); + + menu_->addAction(tr("&Open Bridge"), onMac ? QKeySequence("Ctrl+O") : noShortcut, &app().backend(), &QMLBackend::showMainWindow); + menu_->addAction(tr("&Help"), onMac ? QKeySequence("Ctrl+F1") : noShortcut, &app().backend(), &QMLBackend::showHelp); + menu_->addAction(tr("&Settings"), onMac ? QKeySequence("Ctrl+,") : noShortcut, &app().backend(), &QMLBackend::showSettings); menu_->addSeparator(); - menu_->addAction(tr("&Quit Bridge"), QKeySequence("Ctrl+Q"), &app().backend(), &QMLBackend::quit); + menu_->addAction(tr("&Quit Bridge"), onMac ? QKeySequence("Ctrl+Q") : noShortcut, &app().backend(), &QMLBackend::quit); } From eee2c73a61c7f2670f905e9e020619e31f3db740 Mon Sep 17 00:00:00 2001 From: Xavier Michelon Date: Thu, 4 May 2023 19:57:17 +0200 Subject: [PATCH 10/43] feat(GODT-2610): re-use previous password when removing and adding back account. --- internal/vault/password_archive.go | 46 ++++++++++++++++++++++++ internal/vault/settings_test.go | 27 ++++++++++++++ internal/vault/types_password_archive.go | 25 +++++++++++++ internal/vault/types_settings.go | 4 +++ internal/vault/types_user.go | 4 +-- internal/vault/vault.go | 12 +++++-- tests/bdd_test.go | 2 ++ tests/ctx_bridge_test.go | 1 + tests/ctx_test.go | 2 ++ tests/features/user/relogin.feature | 11 +++++- tests/user_test.go | 33 +++++++++++++++++ 11 files changed, 161 insertions(+), 6 deletions(-) create mode 100644 internal/vault/password_archive.go create mode 100644 internal/vault/types_password_archive.go diff --git a/internal/vault/password_archive.go b/internal/vault/password_archive.go new file mode 100644 index 00000000..cf471adb --- /dev/null +++ b/internal/vault/password_archive.go @@ -0,0 +1,46 @@ +// Copyright (c) 2023 Proton AG +// +// This file is part of Proton Mail Bridge. +// +// Proton Mail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Proton Mail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Proton Mail Bridge. If not, see . + +package vault + +import ( + "crypto/sha256" + "fmt" +) + +// set archives the password for an email address, overwriting any existing archived value. +func (p *PasswordArchive) set(emailAddress string, password []byte) { + if p.Archive == nil { + p.Archive = make(map[string][]byte) + } + + p.Archive[emailHashString(emailAddress)] = password +} + +// get retrieves the archived password for an email address, or nil if not found. +func (p *PasswordArchive) get(emailAddress string) []byte { + if p.Archive == nil { + return nil + } + + return p.Archive[emailHashString(emailAddress)] +} + +// emailHashString returns a hash string for an email address as a hexadecimal string. +func emailHashString(emailAddress string) string { + return fmt.Sprintf("%x", sha256.Sum256([]byte(emailAddress))) +} diff --git a/internal/vault/settings_test.go b/internal/vault/settings_test.go index 3996b9ae..37efda0f 100644 --- a/internal/vault/settings_test.go +++ b/internal/vault/settings_test.go @@ -238,3 +238,30 @@ func TestVault_Settings_LastUserAgent(t *testing.T) { // Check the default first start value. require.Equal(t, vault.DefaultUserAgent, s.GetLastUserAgent()) } + +func Test_Settings_PasswordArchive(t *testing.T) { + // Create a new test vault. + s := newVault(t) + + // The store should have no users. + require.Empty(t, s.GetUserIDs()) + + // Create a new user. + user, err := s.AddUser("userID1", "username1", "username1@pm.me", "authUID1", "authRef1", []byte("keyPass1")) + require.NoError(t, err) + bridgePass := user.BridgePass() + + // Remove the user. + require.NoError(t, user.Close()) + require.NoError(t, s.DeleteUser("userID1")) + + // Add a different user. Another password is generated. + user, err = s.AddUser("userID2", "username2", "username2@pm.me", "authUID2", "authRef2", []byte("keyPass2")) + require.NoError(t, err) + require.NotEqual(t, user.BridgePass(), bridgePass) + + // Add the first user again. The password is restored. + user, err = s.AddUser("userID1", "username1", "username1@pm.me", "authUID1", "authRef1", []byte("keyPass1")) + require.NoError(t, err) + require.Equal(t, user.BridgePass(), bridgePass) +} diff --git a/internal/vault/types_password_archive.go b/internal/vault/types_password_archive.go new file mode 100644 index 00000000..715727f4 --- /dev/null +++ b/internal/vault/types_password_archive.go @@ -0,0 +1,25 @@ +// Copyright (c) 2023 Proton AG +// +// This file is part of Proton Mail Bridge. +// +// Proton Mail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Proton Mail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Proton Mail Bridge. If not, see . + +package vault + +// PasswordArchive maps a list email address hashes to passwords. +// The type is not defined as a map alias to prevent having to handle nil default values when vault was created by an older version of the application. +type PasswordArchive struct { + // we store the SHA-256 sum as string for readability and JSON marshalling of map[[32]byte][]byte will not be allowed, thus breaking vault-editor. + Archive map[string][]byte +} diff --git a/internal/vault/types_settings.go b/internal/vault/types_settings.go index d6961673..158791ea 100644 --- a/internal/vault/types_settings.go +++ b/internal/vault/types_settings.go @@ -53,6 +53,8 @@ type Settings struct { LastHeartbeatSent time.Time + PasswordArchive PasswordArchive + // **WARNING**: These entry can't be removed until they vault has proper migration support. SyncWorkers int SyncAttPool int @@ -105,5 +107,7 @@ func newDefaultSettings(gluonDir string) Settings { LastUserAgent: DefaultUserAgent, LastHeartbeatSent: time.Time{}, + + PasswordArchive: PasswordArchive{}, } } diff --git a/internal/vault/types_user.go b/internal/vault/types_user.go index a41085fb..9da47835 100644 --- a/internal/vault/types_user.go +++ b/internal/vault/types_user.go @@ -73,7 +73,7 @@ func (status SyncStatus) IsComplete() bool { return status.HasLabels && status.HasMessages } -func newDefaultUser(userID, username, primaryEmail, authUID, authRef string, keyPass []byte) UserData { +func newDefaultUser(userID, username, primaryEmail, authUID, authRef string, keyPass, bridgePass []byte) UserData { return UserData{ UserID: userID, Username: username, @@ -82,7 +82,7 @@ func newDefaultUser(userID, username, primaryEmail, authUID, authRef string, key GluonKey: newRandomToken(32), GluonIDs: make(map[string]string), UIDValidity: make(map[string]imap.UID), - BridgePass: newRandomToken(16), + BridgePass: bridgePass, AddressMode: CombinedMode, AuthUID: authUID, diff --git a/internal/vault/vault.go b/internal/vault/vault.go index 0b1c7002..e57c623f 100644 --- a/internal/vault/vault.go +++ b/internal/vault/vault.go @@ -133,7 +133,8 @@ func (vault *Vault) ForUser(parallelism int, fn func(*User) error) error { } // AddUser creates a new user in the vault with the given ID, username and password. -// A bridge password and gluon key are generated using the package's token generator. +// A gluon key is generated using the package's token generator. If a password is found in the password archive for this user, +// it is restored, otherwise a new bridge password is generated using the package's token generator. func (vault *Vault) AddUser(userID, username, primaryEmail, authUID, authRef string, keyPass []byte) (*User, error) { logrus.WithField("userID", userID).Info("Adding vault user") @@ -145,7 +146,12 @@ func (vault *Vault) AddUser(userID, username, primaryEmail, authUID, authRef str }); idx >= 0 { exists = true } else { - data.Users = append(data.Users, newDefaultUser(userID, username, primaryEmail, authUID, authRef, keyPass)) + bridgePass := data.Settings.PasswordArchive.get(primaryEmail) + if len(bridgePass) == 0 { + bridgePass = newRandomToken(16) + } + + data.Users = append(data.Users, newDefaultUser(userID, username, primaryEmail, authUID, authRef, keyPass, bridgePass)) } }); err != nil { return nil, err @@ -177,7 +183,7 @@ func (vault *Vault) DeleteUser(userID string) error { if idx < 0 { return } - + data.Settings.PasswordArchive.set(data.Users[idx].PrimaryEmail, data.Users[idx].BridgePass) data.Users = append(data.Users[:idx], data.Users[idx+1:]...) }) } diff --git a/tests/bdd_test.go b/tests/bdd_test.go index 1dadb870..def2ac15 100644 --- a/tests/bdd_test.go +++ b/tests/bdd_test.go @@ -180,6 +180,8 @@ func TestFeatures(testingT *testing.T) { ctx.Step(`^user "([^"]*)" is not listed$`, s.userIsNotListed) ctx.Step(`^user "([^"]*)" finishes syncing$`, s.userFinishesSyncing) ctx.Step(`^user "([^"]*)" has telemetry set to (\d+)$`, s.userHasTelemetrySetTo) + ctx.Step(`^the bridge password of user "([^"]*)" is changed to "([^"]*)"`, s.bridgePasswordOfUserIsChangedTo) + ctx.Step(`^the bridge password of user "([^"]*)" is equal to "([^"]*)"`, s.bridgePasswordOfUserIsEqualTo) // ==== IMAP ==== ctx.Step(`^user "([^"]*)" connects IMAP client "([^"]*)"$`, s.userConnectsIMAPClient) diff --git a/tests/ctx_bridge_test.go b/tests/ctx_bridge_test.go index 5c62b0bf..b153ac39 100644 --- a/tests/ctx_bridge_test.go +++ b/tests/ctx_bridge_test.go @@ -114,6 +114,7 @@ func (t *testCtx) initBridge() (<-chan events.Event, error) { } else if corrupt { return nil, fmt.Errorf("vault is corrupt") } + t.vault = vault // Create the underlying cookie jar. jar, err := cookiejar.New(nil) diff --git a/tests/ctx_test.go b/tests/ctx_test.go index 369e865f..5fad8fee 100644 --- a/tests/ctx_test.go +++ b/tests/ctx_test.go @@ -36,6 +36,7 @@ import ( "github.com/ProtonMail/proton-bridge/v3/internal/bridge" frontend "github.com/ProtonMail/proton-bridge/v3/internal/frontend/grpc" "github.com/ProtonMail/proton-bridge/v3/internal/locations" + "github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/bradenaw/juniper/xslices" "github.com/cucumber/godog" "github.com/emersion/go-imap/client" @@ -135,6 +136,7 @@ type testCtx struct { // bridge holds the bridge app under test. bridge *bridge.Bridge + vault *vault.Vault // service holds the gRPC frontend service under test. service *frontend.Service diff --git a/tests/features/user/relogin.feature b/tests/features/user/relogin.feature index 021a6696..faf5f5cd 100644 --- a/tests/features/user/relogin.feature +++ b/tests/features/user/relogin.feature @@ -12,4 +12,13 @@ Feature: A logged out user can login again Scenario: Cannot login to removed account When user "[user:user]" is deleted - Then user "[user:user]" is not listed \ No newline at end of file + Then user "[user:user]" is not listed + + Scenario: Bridge password persists after logout/login + Given there exists an account with username "testUser" and password "password" + And the user logs in with username "testUser" and password "password" + And the bridge password of user "testUser" is changed to "YnJpZGdlcGFzc3dvcmQK" + And user "testUser" is deleted + And the user logs in with username "testUser" and password "password" + Then user "testUser" is eventually listed and connected + And the bridge password of user "testUser" is equal to "YnJpZGdlcGFzc3dvcmQK" diff --git a/tests/user_test.go b/tests/user_test.go index 17542987..c06f0bd7 100644 --- a/tests/user_test.go +++ b/tests/user_test.go @@ -28,6 +28,8 @@ import ( "github.com/ProtonMail/go-proton-api" "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/proton-bridge/v3/internal/bridge" + "github.com/ProtonMail/proton-bridge/v3/internal/vault" + "github.com/ProtonMail/proton-bridge/v3/pkg/algo" "github.com/bradenaw/juniper/iterator" "github.com/bradenaw/juniper/xslices" "github.com/cucumber/godog" @@ -426,6 +428,37 @@ func (s *scenario) userHasTelemetrySetTo(username string, telemetry int) error { }) } +func (s *scenario) bridgePasswordOfUserIsChangedTo(username, bridgePassword string) error { + b, err := algo.B64RawDecode([]byte(bridgePassword)) + if err != nil { + return errors.New("the password is not base64 encoded") + } + + var setErr error + if err := s.t.vault.GetUser( + s.t.getUserByName(username).getUserID(), + func(user *vault.User) { setErr = user.SetBridgePass(b) }, + ); err != nil { + return err + } + + return setErr +} + +func (s *scenario) bridgePasswordOfUserIsEqualTo(username, bridgePassword string) error { + userInfo, err := s.t.bridge.QueryUserInfo(username) + if err != nil { + return err + } + + readPassword := string(userInfo.BridgePass) + if readPassword != bridgePassword { + return fmt.Errorf("bridge password mismatch, expected '%v', got '%v'", bridgePassword, readPassword) + } + + return nil +} + func (s *scenario) addAdditionalAddressToAccount(username, address string, disabled bool) error { userID := s.t.getUserByName(username).getUserID() From ad02c71ad6b3ad69e7e48f618cc4a8d451b9a3b2 Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Mon, 8 May 2023 13:18:34 +0200 Subject: [PATCH 11/43] fix(GODT-2616): Silence out of Order UID report https://github.com/ProtonMail/gluon/pull/348 --- go.mod | 2 +- go.sum | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index fb5c2ea2..4819274a 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 github.com/Masterminds/semver/v3 v3.2.0 - github.com/ProtonMail/gluon v0.16.1-0.20230504091128-d8a3371a4ba5 + github.com/ProtonMail/gluon v0.16.1-0.20230508105645-e4f4a844ccae github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a github.com/ProtonMail/go-proton-api v0.4.1-0.20230426081144-f77778bae1be github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton diff --git a/go.sum b/go.sum index 1194b086..d4dba3bc 100644 --- a/go.sum +++ b/go.sum @@ -28,10 +28,8 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo= github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk= github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g= -github.com/ProtonMail/gluon v0.16.1-0.20230428090920-2797a1764f16 h1:X5kb4PwVrgVDQjBkpiobYrDlqKDMuS1o92Ty+rZ1ptE= -github.com/ProtonMail/gluon v0.16.1-0.20230428090920-2797a1764f16/go.mod h1:yA4hk6CJw0BMo+YL8Y3ckCYs5L20sysu9xseshwY3QI= -github.com/ProtonMail/gluon v0.16.1-0.20230504091128-d8a3371a4ba5 h1:plqDYC9wOaO/D/QJWMB1nwjjOkT3bIIiXrgounnaOhc= -github.com/ProtonMail/gluon v0.16.1-0.20230504091128-d8a3371a4ba5/go.mod h1:yA4hk6CJw0BMo+YL8Y3ckCYs5L20sysu9xseshwY3QI= +github.com/ProtonMail/gluon v0.16.1-0.20230508105645-e4f4a844ccae h1:3p8P21+BoAYj1nSswdwQvc7jr2lixuVFpWE4QlvA8f0= +github.com/ProtonMail/gluon v0.16.1-0.20230508105645-e4f4a844ccae/go.mod h1:yA4hk6CJw0BMo+YL8Y3ckCYs5L20sysu9xseshwY3QI= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= From bda158d6c6840090691b02faf707eb902e774363 Mon Sep 17 00:00:00 2001 From: Romain LE JEUNE Date: Fri, 5 May 2023 16:07:01 +0200 Subject: [PATCH 12/43] feat(GODT-2346): treat external address as disabled one. --- go.mod | 2 +- go.sum | 4 ++-- internal/bridge/user_test.go | 19 +++++++++++++++++++ internal/user/user.go | 2 +- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 4819274a..465b3829 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.2.0 github.com/ProtonMail/gluon v0.16.1-0.20230508105645-e4f4a844ccae github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a - github.com/ProtonMail/go-proton-api v0.4.1-0.20230426081144-f77778bae1be + github.com/ProtonMail/go-proton-api v0.4.1-0.20230505091503-167f3d239b0c github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton github.com/PuerkitoBio/goquery v1.8.1 github.com/abiosoft/ishell v2.0.0+incompatible diff --git a/go.sum b/go.sum index d4dba3bc..4a7c0b88 100644 --- a/go.sum +++ b/go.sum @@ -39,8 +39,8 @@ github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753 h1:I8IsYA297 github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= -github.com/ProtonMail/go-proton-api v0.4.1-0.20230426081144-f77778bae1be h1:TNHnEyUQDf97CRGCFWLxg7I5ASSEMO3TN2lbNw2cD6U= -github.com/ProtonMail/go-proton-api v0.4.1-0.20230426081144-f77778bae1be/go.mod h1:UkrG9gN2o9mzdx/an0XRc6a4s5Haef1A7Eyd2iXlw28= +github.com/ProtonMail/go-proton-api v0.4.1-0.20230505091503-167f3d239b0c h1:uqo3mKt4ffhqPFLVV7VxjuN12DAFQmqEju/Wy5dk6Rk= +github.com/ProtonMail/go-proton-api v0.4.1-0.20230505091503-167f3d239b0c/go.mod h1:UkrG9gN2o9mzdx/an0XRc6a4s5Haef1A7Eyd2iXlw28= github.com/ProtonMail/go-srp v0.0.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg= github.com/ProtonMail/go-srp v0.0.5/go.mod h1:06iYHtLXW8vjLtccWj++x3MKy65sIT8yZd7nrJF49rs= github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton h1:YS6M20yvjCJPR1r4ADW5TPn6rahs4iAyZaACei86bEc= diff --git a/internal/bridge/user_test.go b/internal/bridge/user_test.go index 135c8f23..d8767110 100644 --- a/internal/bridge/user_test.go +++ b/internal/bridge/user_test.go @@ -708,6 +708,25 @@ func TestBridge_User_Refresh(t *testing.T) { }) } +func TestBridge_User_GetAddresses(t *testing.T) { + withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) { + // Create a user. + userID, _, err := s.CreateUser("user", password) + require.NoError(t, err) + addrID2, err := s.CreateAddress(userID, "user@external.com", []byte("password")) + require.NoError(t, err) + require.NoError(t, s.ChangeAddressType(userID, addrID2, proton.AddressTypeExternal)) + + withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) { + userLoginAndSync(ctx, t, bridge, "user", password) + info, err := bridge.GetUserInfo(userID) + require.NoError(t, err) + require.Equal(t, 1, len(info.Addresses)) + require.Equal(t, info.Addresses[0], "user@proton.local") + }) + }) +} + // getErr returns the error that was passed to it. func getErr[T any](_ T, err error) error { return err diff --git a/internal/user/user.go b/internal/user/user.go index d8418330..3e601b9c 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -282,7 +282,7 @@ func (user *User) Match(query string) bool { func (user *User) Emails() []string { return safe.RLockRet(func() []string { addresses := xslices.Filter(maps.Values(user.apiAddrs), func(addr proton.Address) bool { - return addr.Status == proton.AddressStatusEnabled + return addr.Status == proton.AddressStatusEnabled && addr.Type != proton.AddressTypeExternal }) slices.SortFunc(addresses, func(a, b proton.Address) bool { From 0417e495ae42bdf68e4a01780d64394846451f69 Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Mon, 8 May 2023 15:21:56 +0200 Subject: [PATCH 13/43] fix(GODT-2618): Crash when address does not have unlocked keyring --- internal/user/sync.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/internal/user/sync.go b/internal/user/sync.go index 87ba6ca1..677d0ffc 100644 --- a/internal/user/sync.go +++ b/internal/user/sync.go @@ -513,7 +513,16 @@ func (user *User) syncMessages( result, err := parallel.MapContext(ctx, maxMessagesInParallel, chunk, func(ctx context.Context, msg proton.FullMessage) (*buildRes, error) { defer async.HandlePanic(user.panicHandler) - return buildRFC822(apiLabels, msg, addrKRs[msg.AddressID], new(bytes.Buffer)), nil + kr, ok := addrKRs[msg.AddressID] + if !ok { + return &buildRes{ + messageID: msg.ID, + addressID: msg.AddressID, + err: fmt.Errorf("address does not have an unlocked keyring"), + }, nil + } + + return buildRFC822(apiLabels, msg, kr, new(bytes.Buffer)), nil }) if err != nil { return From c438704648448f2e303bf79ce2a78fd9bc78b2b2 Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Tue, 9 May 2023 08:30:38 +0200 Subject: [PATCH 14/43] test: Disable sync open files test It is os specific and it has a tendency to succeed on CI runners. --- internal/bridge/sync_unix_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/bridge/sync_unix_test.go b/internal/bridge/sync_unix_test.go index 21b97924..5d046f97 100644 --- a/internal/bridge/sync_unix_test.go +++ b/internal/bridge/sync_unix_test.go @@ -31,7 +31,8 @@ import ( "github.com/stretchr/testify/require" ) -func TestBridge_SyncExistsWithErrorWhenTooManyFilesAreOpen(t *testing.T) { +// Disabled due to flakyness. +func _TestBridge_SyncExistsWithErrorWhenTooManyFilesAreOpen(t *testing.T) { //nolint:unused var rlimitCurrent syscall.Rlimit require.NoError(t, syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlimitCurrent)) From a05f93debd9df80dc62bee6dad21ba18565ae88c Mon Sep 17 00:00:00 2001 From: Romain LE JEUNE Date: Tue, 9 May 2023 08:49:58 +0200 Subject: [PATCH 15/43] feat(GODT-2520): Update error message for free users. --- .../bridge-gui/qml/Notifications/Notifications.qml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/frontend/bridge-gui/bridge-gui/qml/Notifications/Notifications.qml b/internal/frontend/bridge-gui/bridge-gui/qml/Notifications/Notifications.qml index 1431aa36..96f13669 100644 --- a/internal/frontend/bridge-gui/bridge-gui/qml/Notifications/Notifications.qml +++ b/internal/frontend/bridge-gui/bridge-gui/qml/Notifications/Notifications.qml @@ -535,11 +535,12 @@ QtObject { } property Notification onlyPaidUsers: Notification { - description: qsTr("Bridge is exclusive to our paid plans. Upgrade your account to use Bridge.") + description: qsTr("Bridge is exclusive to our mail paid plans. Upgrade your account to use Bridge.") brief: qsTr("Upgrade your account") icon: "./icons/ic-exclamation-circle-filled.svg" type: Notification.NotificationType.Danger group: Notifications.Group.Configuration + property var pricingLink: "https://proton.me/mail/pricing" Connections { target: Backend @@ -550,8 +551,9 @@ QtObject { action: [ Action { - text: qsTr("OK") + text: qsTr("Upgrade") onTriggered: { + Qt.openUrlExternally(root.onlyPaidUsers.pricingLink) root.onlyPaidUsers.active = false } } From 36342299c7cb754b172084e395f60d3f6ab4f461 Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Tue, 9 May 2023 09:33:19 +0200 Subject: [PATCH 16/43] chore: Set default log level to Debug --- internal/logging/logging.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 667f29c4..483cf395 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -113,7 +113,7 @@ func Init(logsPath, level string) error { // Debug or Trace. func setLevel(level string) error { if level == "" { - return nil + level = "debug" } logLevel, err := logrus.ParseLevel(level) From e606d98664997eb24df983e63ec1a8714c54d51a Mon Sep 17 00:00:00 2001 From: Xavier Michelon Date: Mon, 8 May 2023 17:52:23 +0200 Subject: [PATCH 17/43] fix(GODT-2613): install the TLS certificate in the user keychain. --- internal/certs/cert_store_darwin.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/certs/cert_store_darwin.go b/internal/certs/cert_store_darwin.go index 61a5977d..97a79fa5 100644 --- a/internal/certs/cert_store_darwin.go +++ b/internal/certs/cert_store_darwin.go @@ -50,7 +50,7 @@ int installTrustedCert(char const *bytes, unsigned long long length) { (id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultTrustRoot], (id)kSecTrustSettingsPolicy: (__bridge id) policy, }; - status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainAdmin, (__bridge CFTypeRef)(trustSettings)); + status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings)); CFRelease(policy); CFRelease(cert); @@ -72,7 +72,7 @@ int removeTrustedCert(char const *bytes, unsigned long long length) { (id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultUnspecified], (id)kSecTrustSettingsPolicy: (__bridge id) policy, }; - OSStatus status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainAdmin, (__bridge CFTypeRef)(trustSettings)); + OSStatus status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings)); CFRelease(policy); if (errSecSuccess != status) { CFRelease(cert); From 51288791c09c198f2af2e9e1c0a3f72e0da4bc09 Mon Sep 17 00:00:00 2001 From: Romain LE JEUNE Date: Wed, 10 May 2023 15:53:49 +0200 Subject: [PATCH 18/43] fix(GODT-2527): Cleanup 503 test since handled by GPA. --- internal/bridge/user_event_test.go | 42 ------------------------------ 1 file changed, 42 deletions(-) diff --git a/internal/bridge/user_event_test.go b/internal/bridge/user_event_test.go index fa69ea1f..c5356a07 100644 --- a/internal/bridge/user_event_test.go +++ b/internal/bridge/user_event_test.go @@ -843,48 +843,6 @@ func TestBridge_User_HandleParentLabelRename(t *testing.T) { }) } -// TBD: GODT-2527. -func _TestBridge503DuringEventDoesNotCauseBadEvent(t *testing.T) { //nolint:unused,deadcode - withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) { - // Create a user. - userID, addrID, err := s.CreateUser("user", password) - require.NoError(t, err) - - labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder) - require.NoError(t, err) - - // Create 10 messages for the user. - withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) { - createNumMessages(ctx, t, c, addrID, labelID, 10) - }) - - withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { - userLoginAndSync(ctx, t, bridge, "user", password) - - var messageIDs []string - - // Create 10 more messages for the user, generating events. - withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) { - messageIDs = createNumMessages(ctx, t, c, addrID, labelID, 10) - }) - - mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).MinTimes(1) - - s.AddStatusHook(func(req *http.Request) (int, bool) { - if xslices.Index(xslices.Map(messageIDs[0:5], func(messageID string) string { - return "/mail/v4/messages/" + messageID - }), req.URL.Path) < 0 { - return 0, false - } - - return http.StatusServiceUnavailable, true - }) - - userContinueEventProcess(ctx, t, s, bridge) - }) - }) -} - // userLoginAndSync logs in user and waits until user is fully synced. func userLoginAndSync( ctx context.Context, From 6ba8052a1efb02c03b8cc31f35be8f0d5846cf42 Mon Sep 17 00:00:00 2001 From: Xavier Michelon Date: Wed, 10 May 2023 14:40:41 +0200 Subject: [PATCH 19/43] feat(GODT-2621): display pop up warning when IMAP login fails because user is locked (connecting). --- internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp | 10 ++++++++++ .../frontend/bridge-gui/bridgepp/bridgepp/User/User.h | 1 + 2 files changed, 11 insertions(+) diff --git a/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp b/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp index 47e8e2db..b10002a4 100644 --- a/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp +++ b/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp @@ -1019,6 +1019,16 @@ void QMLBackend::onIMAPLoginFailed(QString const &username) { tr("Your email client can't connect to Proton Bridge. Make sure you are using the local Bridge password shown in Bridge.")); break; + case UserState::Locked: + if (user->isNotificationInCooldown(User::ENotification::IMAPLoginWhileLocked)) { + return; + } + user->startNotificationCooldownPeriod(User::ENotification::IMAPLoginWhileLocked, cooldownDurationMs); + emit selectUser(user->id(), false); + trayIcon_->showErrorPopupNotification(tr("Connection in progress"), + tr("Your Proton account in Bridge is being connected. Please wait or restart Bridge.")); + break; + default: break; } diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/User/User.h b/internal/frontend/bridge-gui/bridgepp/bridgepp/User/User.h index d30c6b00..b63e51d6 100644 --- a/internal/frontend/bridge-gui/bridgepp/bridgepp/User/User.h +++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/User/User.h @@ -66,6 +66,7 @@ public: // data types enum class ENotification { IMAPLoginWhileSignedOut, ///< An IMAP client tried to login while the user is signed out. IMAPPasswordFailure, ///< An IMAP client provided an invalid password for the user. + IMAPLoginWhileLocked, ///< An IMAP client tried to connect while the user is locked. }; public: // static member function From edac2419f93a0cbca8e0b0995877b0e9bc2b16d5 Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Fri, 5 May 2023 11:23:17 +0200 Subject: [PATCH 20/43] feat(GODT-2585): Add CPC utility Add Channel based RPC utilities. --- cpc/cpc.go | 138 ++++++++++++++++++++++++++++++++++++++++++++++++ cpc/cpc_test.go | 63 ++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 cpc/cpc.go create mode 100644 cpc/cpc_test.go diff --git a/cpc/cpc.go b/cpc/cpc.go new file mode 100644 index 00000000..31185382 --- /dev/null +++ b/cpc/cpc.go @@ -0,0 +1,138 @@ +// Copyright (c) 2023 Proton AG +// +// This file is part of Proton Mail Bridge. +// +// Proton Mail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Proton Mail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Proton Mail Bridge. If not, see . + +package cpc + +import ( + "context" + "errors" +) + +var ErrRequestHasNoReply = errors.New("request has no reply channel") +var ErrExpectedReply = errors.New("request does not have reply channel") + +// Utilities to implement Chanel Procedure Calls. Similar in concept to RPC, but with between go-routines. + +type RequestReply struct { + Value any + Error error +} + +type Request struct { + Value any + Reply chan RequestReply +} + +func NewRequest(value any) *Request { + return &Request{ + Value: value, + Reply: make(chan RequestReply), + } +} + +func NewRequestWithoutReply(value any) *Request { + return &Request{ + Value: value, + Reply: nil, + } +} + +func (r *Request) Close() { + if r.Reply != nil { + panic("request reply has not been sent") + } +} + +func (r *Request) SendReply(ctx context.Context, value any, err error) { + if r.Reply == nil { + panic("request has no reply") + } + + defer func() { + close(r.Reply) + r.Reply = nil + }() + + select { + case <-ctx.Done(): + case r.Reply <- RequestReply{ + Value: value, + Error: err, + }: + } +} + +type CPC struct { + request chan *Request +} + +func NewCPC() *CPC { + return &CPC{ + request: make(chan *Request), + } +} + +// Receive is meant to be called by the code that is supposed to handle the requests that arrive. +func (c *CPC) Receive(ctx context.Context, f func(context.Context, *Request)) { + for request := range c.request { + f(ctx, request) + request.Close() + } +} + +func (c *CPC) Close() { + close(c.request) +} + +// SendNoReply sends a request which doesn't expect a reply. +func (c *CPC) SendNoReply(ctx context.Context, value any) error { + return c.executeNoReplyImpl(ctx, NewRequestWithoutReply(value)) +} + +// SendWithReply sends a request which expects a reply. +func (c *CPC) SendWithReply(ctx context.Context, value any) (any, error) { + return c.executeReplyImpl(ctx, NewRequest(value)) +} + +func (c *CPC) executeNoReplyImpl(ctx context.Context, request *Request) error { + select { + case <-ctx.Done(): + return ctx.Err() + case c.request <- request: + } + + return nil +} + +func (c *CPC) executeReplyImpl(ctx context.Context, request *Request) (any, error) { + if request.Reply == nil { + return nil, ErrExpectedReply + } + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case c.request <- request: + } + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case reply := <-request.Reply: + return reply.Value, reply.Error + } +} diff --git a/cpc/cpc_test.go b/cpc/cpc_test.go new file mode 100644 index 00000000..8fc70406 --- /dev/null +++ b/cpc/cpc_test.go @@ -0,0 +1,63 @@ +// Copyright (c) 2023 Proton AG +// +// This file is part of Proton Mail Bridge. +// +// Proton Mail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Proton Mail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Proton Mail Bridge. If not, see . + +package cpc + +import ( + "context" + "sync" + "testing" + + "github.com/stretchr/testify/require" +) + +type sendIntRequest struct{} + +type quitRequest struct{} + +func TestCPC_Receive(t *testing.T) { + const replyValue = 20 + + cpc := NewCPC() + + wg := sync.WaitGroup{} + + go func() { + defer wg.Done() + + wg.Add(1) + + cpc.Receive(context.Background(), func(ctx context.Context, request *Request) { + switch request.Value.(type) { + case sendIntRequest: + request.SendReply(ctx, replyValue, nil) + case quitRequest: + cpc.Close() + default: + panic("unknown request") + } + }) + }() + + r, err := cpc.SendWithReply(context.Background(), sendIntRequest{}) + require.NoError(t, err) + require.Equal(t, r, replyValue) + + require.NoError(t, cpc.SendNoReply(context.Background(), quitRequest{})) + + wg.Wait() +} From fb4a0e77afa8e9af345d7aaa98fe7e7ba1f9ca98 Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Tue, 9 May 2023 11:15:30 +0200 Subject: [PATCH 21/43] feat(GODT-2585): Server Manager Add a dedicated go-routine whose sole responsibility is to manage the life time of the IMAP and SMTP servers and their listeners. The current implementation behaves the same way as the previous state. The new behavior will be implemented in a follow MR. --- cpc/cpc.go | 21 + internal/bridge/bridge.go | 72 +-- internal/bridge/bridge_test.go | 4 +- internal/bridge/configure.go | 5 +- internal/bridge/imap.go | 199 +------ internal/bridge/server_manager.go | 620 ++++++++++++++++++++++ internal/bridge/settings.go | 75 +-- internal/bridge/settings_test.go | 8 +- internal/bridge/smtp.go | 83 +-- internal/bridge/user_events.go | 16 +- internal/frontend/cli/accounts.go | 2 +- internal/frontend/cli/system.go | 8 +- internal/frontend/grpc/service_methods.go | 10 +- internal/frontend/grpc/service_user.go | 4 +- tests/bridge_test.go | 8 +- 15 files changed, 697 insertions(+), 438 deletions(-) create mode 100644 internal/bridge/server_manager.go diff --git a/cpc/cpc.go b/cpc/cpc.go index 31185382..dbe9bc29 100644 --- a/cpc/cpc.go +++ b/cpc/cpc.go @@ -20,6 +20,7 @@ package cpc import ( "context" "errors" + "fmt" ) var ErrRequestHasNoReply = errors.New("request has no reply channel") @@ -94,6 +95,10 @@ func (c *CPC) Receive(ctx context.Context, f func(context.Context, *Request)) { } } +func (c *CPC) ReceiveCh() <-chan *Request { + return c.request +} + func (c *CPC) Close() { close(c.request) } @@ -108,6 +113,22 @@ func (c *CPC) SendWithReply(ctx context.Context, value any) (any, error) { return c.executeReplyImpl(ctx, NewRequest(value)) } +func SendWithReplyType[T any](ctx context.Context, c *CPC, value any) (T, error) { + val, err := c.executeReplyImpl(ctx, NewRequest(value)) + if err != nil { + var t T + return t, err + } + + switch vt := val.(type) { + case T: + return vt, nil + default: + var t T + return t, fmt.Errorf("reply type does not match") + } +} + func (c *CPC) executeNoReplyImpl(ctx context.Context, request *Request) error { select { case <-ctx.Done(): diff --git a/internal/bridge/bridge.go b/internal/bridge/bridge.go index 29a87b1f..92b34f33 100644 --- a/internal/bridge/bridge.go +++ b/internal/bridge/bridge.go @@ -29,7 +29,6 @@ import ( "time" "github.com/Masterminds/semver/v3" - "github.com/ProtonMail/gluon" "github.com/ProtonMail/gluon/async" imapEvents "github.com/ProtonMail/gluon/events" "github.com/ProtonMail/gluon/imap" @@ -45,7 +44,6 @@ import ( "github.com/ProtonMail/proton-bridge/v3/internal/user" "github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/bradenaw/juniper/xslices" - "github.com/emersion/go-smtp" "github.com/go-resty/resty/v2" "github.com/sirupsen/logrus" ) @@ -67,13 +65,7 @@ type Bridge struct { tlsConfig *tls.Config // imapServer is the bridge's IMAP server. - imapServer *gluon.Server - imapListener net.Listener - imapEventCh chan imapEvents.Event - - // smtpServer is the bridge's SMTP server. - smtpServer *smtp.Server - smtpListener net.Listener + imapEventCh chan imapEvents.Event // updater is the bridge's updater. updater Updater @@ -134,6 +126,8 @@ type Bridge struct { goHeartbeat func() uidValidityGenerator imap.UIDValidityGenerator + + serverManager *ServerManager } // New creates a new bridge. @@ -224,16 +218,6 @@ func newBridge( return nil, fmt.Errorf("failed to load TLS config: %w", err) } - gluonCacheDir, err := getGluonDir(vault) - if err != nil { - return nil, fmt.Errorf("failed to get Gluon directory: %w", err) - } - - gluonDataDir, err := locator.ProvideGluonDataPath() - if err != nil { - return nil, fmt.Errorf("failed to get Gluon Database directory: %w", err) - } - firstStart := vault.GetFirstStart() if err := vault.SetFirstStart(false); err != nil { return nil, fmt.Errorf("failed to save first start indicator: %w", err) @@ -246,23 +230,6 @@ func newBridge( identifier.SetClientString(vault.GetLastUserAgent()) - imapServer, err := newIMAPServer( - gluonCacheDir, - gluonDataDir, - curVersion, - tlsConfig, - reporter, - logIMAPClient, - logIMAPServer, - imapEventCh, - tasks, - uidValidityGenerator, - panicHandler, - ) - if err != nil { - return nil, fmt.Errorf("failed to create IMAP server: %w", err) - } - focusService, err := focus.NewService(locator, curVersion, panicHandler) if err != nil { return nil, fmt.Errorf("failed to create focus service: %w", err) @@ -279,7 +246,6 @@ func newBridge( identifier: identifier, tlsConfig: tlsConfig, - imapServer: imapServer, imapEventCh: imapEventCh, updater: updater, @@ -306,9 +272,13 @@ func newBridge( tasks: tasks, uidValidityGenerator: uidValidityGenerator, + + serverManager: newServerManager(), } - bridge.smtpServer = newSMTPServer(bridge, tlsConfig, logSMTP) + if err := bridge.serverManager.Init(bridge); err != nil { + return nil, err + } return bridge, nil } @@ -381,10 +351,6 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error { }) }) - // We need to load users before we can start the IMAP and SMTP servers. - // We must only start the servers once. - var once sync.Once - // Attempt to load users from the vault when triggered. bridge.goLoad = bridge.tasks.Trigger(func(ctx context.Context) { if err := bridge.loadUsers(ctx); err != nil { @@ -396,17 +362,6 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error { } bridge.publish(events.AllUsersLoaded{}) - - // Once all users have been loaded, start the bridge's IMAP and SMTP servers. - once.Do(func() { - if err := bridge.serveIMAP(); err != nil { - logrus.WithError(err).Error("Failed to start IMAP server") - } - - if err := bridge.serveSMTP(); err != nil { - logrus.WithError(err).Error("Failed to start SMTP server") - } - }) }) defer bridge.goLoad() @@ -452,14 +407,9 @@ func (bridge *Bridge) GetErrors() []error { func (bridge *Bridge) Close(ctx context.Context) { logrus.Info("Closing bridge") - // Close the IMAP server. - if err := bridge.closeIMAP(ctx); err != nil { - logrus.WithError(err).Error("Failed to close IMAP server") - } - - // Close the SMTP server. - if err := bridge.closeSMTP(); err != nil { - logrus.WithError(err).Error("Failed to close SMTP server") + // Close the servers + if err := bridge.serverManager.CloseServers(ctx); err != nil { + logrus.WithError(err).Error("Failed to close servers") } // Close all users. diff --git a/internal/bridge/bridge_test.go b/internal/bridge/bridge_test.go index 65939daa..1ac94815 100644 --- a/internal/bridge/bridge_test.go +++ b/internal/bridge/bridge_test.go @@ -834,8 +834,8 @@ func withBridgeNoMocks( waitForEvent(t, eventCh, events.SMTPServerReady{}) // Set random IMAP and SMTP ports for the tests. - require.NoError(t, bridge.SetIMAPPort(0)) - require.NoError(t, bridge.SetSMTPPort(0)) + require.NoError(t, bridge.SetIMAPPort(ctx, 0)) + require.NoError(t, bridge.SetSMTPPort(ctx, 0)) // Close the bridge when done. defer bridge.Close(ctx) diff --git a/internal/bridge/configure.go b/internal/bridge/configure.go index 5b323b0a..6becbff1 100644 --- a/internal/bridge/configure.go +++ b/internal/bridge/configure.go @@ -18,6 +18,7 @@ package bridge import ( + "context" "strings" "github.com/ProtonMail/proton-bridge/v3/internal/clientconfig" @@ -31,7 +32,7 @@ import ( // ConfigureAppleMail configures apple mail for the given userID and address. // If configuring apple mail for Catalina or newer, it ensures Bridge is using SSL. -func (bridge *Bridge) ConfigureAppleMail(userID, address string) error { +func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address string) error { logrus.WithFields(logrus.Fields{ "userID": userID, "address": logging.Sensitive(address), @@ -56,7 +57,7 @@ func (bridge *Bridge) ConfigureAppleMail(userID, address string) error { } if useragent.IsCatalinaOrNewer() && !bridge.vault.GetSMTPSSL() { - if err := bridge.SetSMTPSSL(true); err != nil { + if err := bridge.SetSMTPSSL(ctx, true); err != nil { return err } } diff --git a/internal/bridge/imap.go b/internal/bridge/imap.go index a5d94fe0..f5f30275 100644 --- a/internal/bridge/imap.go +++ b/internal/bridge/imap.go @@ -20,7 +20,6 @@ package bridge import ( "context" "crypto/tls" - "fmt" "io" "os" "path/filepath" @@ -37,203 +36,21 @@ import ( "github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/logging" "github.com/ProtonMail/proton-bridge/v3/internal/user" - "github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/sirupsen/logrus" ) -func (bridge *Bridge) serveIMAP() error { - port, err := func() (int, error) { - if bridge.imapServer == nil { - return 0, fmt.Errorf("no IMAP server instance running") - } - - logrus.WithFields(logrus.Fields{ - "port": bridge.vault.GetIMAPPort(), - "ssl": bridge.vault.GetIMAPSSL(), - }).Info("Starting IMAP server") - - imapListener, err := newListener(bridge.vault.GetIMAPPort(), bridge.vault.GetIMAPSSL(), bridge.tlsConfig) - if err != nil { - return 0, fmt.Errorf("failed to create IMAP listener: %w", err) - } - - bridge.imapListener = imapListener - - if err := bridge.imapServer.Serve(context.Background(), bridge.imapListener); err != nil { - return 0, fmt.Errorf("failed to serve IMAP: %w", err) - } - - if err := bridge.vault.SetIMAPPort(getPort(imapListener.Addr())); err != nil { - return 0, fmt.Errorf("failed to store IMAP port in vault: %w", err) - } - - return getPort(imapListener.Addr()), nil - }() - - if err != nil { - bridge.publish(events.IMAPServerError{ - Error: err, - }) - - return err - } - - bridge.publish(events.IMAPServerReady{ - Port: port, - }) - - return nil -} - -func (bridge *Bridge) restartIMAP() error { - logrus.Info("Restarting IMAP server") - - if bridge.imapListener != nil { - if err := bridge.imapListener.Close(); err != nil { - return fmt.Errorf("failed to close IMAP listener: %w", err) - } - - bridge.publish(events.IMAPServerStopped{}) - } - - return bridge.serveIMAP() -} - -func (bridge *Bridge) closeIMAP(ctx context.Context) error { - logrus.Info("Closing IMAP server") - - if bridge.imapServer != nil { - if err := bridge.imapServer.Close(ctx); err != nil { - return fmt.Errorf("failed to close IMAP server: %w", err) - } - - bridge.imapServer = nil - } - - if bridge.imapListener != nil { - if err := bridge.imapListener.Close(); err != nil { - return fmt.Errorf("failed to close IMAP listener: %w", err) - } - } - - bridge.publish(events.IMAPServerStopped{}) - - return nil +func (bridge *Bridge) restartIMAP(ctx context.Context) error { + return bridge.serverManager.RestartIMAP(ctx) } // addIMAPUser connects the given user to gluon. func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error { - if bridge.imapServer == nil { - return fmt.Errorf("no imap server instance running") - } - - imapConn, err := user.NewIMAPConnectors() - if err != nil { - return fmt.Errorf("failed to create IMAP connectors: %w", err) - } - - for addrID, imapConn := range imapConn { - log := logrus.WithFields(logrus.Fields{ - "userID": user.ID(), - "addrID": addrID, - }) - - if gluonID, ok := user.GetGluonID(addrID); ok { - log.WithField("gluonID", gluonID).Info("Loading existing IMAP user") - - // Load the user, checking whether the DB was newly created. - isNew, err := bridge.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey()) - if err != nil { - return fmt.Errorf("failed to load IMAP user: %w", err) - } - - if isNew { - // If the DB was newly created, clear the sync status; gluon's DB was not found. - logrus.Warn("IMAP user DB was newly created, clearing sync status") - - // Remove the user from IMAP so we can clear the sync status. - if err := bridge.imapServer.RemoveUser(ctx, gluonID, false); err != nil { - return fmt.Errorf("failed to remove IMAP user: %w", err) - } - - // Clear the sync status -- we need to resync all messages. - if err := user.ClearSyncStatus(); err != nil { - return fmt.Errorf("failed to clear sync status: %w", err) - } - - // Add the user back to the IMAP server. - if isNew, err := bridge.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey()); err != nil { - return fmt.Errorf("failed to add IMAP user: %w", err) - } else if isNew { - panic("IMAP user should already have a database") - } - } else if status := user.GetSyncStatus(); !status.HasLabels { - // Otherwise, the DB already exists -- if the labels are not yet synced, we need to re-create the DB. - if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil { - return fmt.Errorf("failed to remove old IMAP user: %w", err) - } - - if err := user.RemoveGluonID(addrID, gluonID); err != nil { - return fmt.Errorf("failed to remove old IMAP user ID: %w", err) - } - - gluonID, err := bridge.imapServer.AddUser(ctx, imapConn, user.GluonKey()) - if err != nil { - return fmt.Errorf("failed to add IMAP user: %w", err) - } - - if err := user.SetGluonID(addrID, gluonID); err != nil { - return fmt.Errorf("failed to set IMAP user ID: %w", err) - } - - log.WithField("gluonID", gluonID).Info("Re-created IMAP user") - } - } else { - log.Info("Creating new IMAP user") - - gluonID, err := bridge.imapServer.AddUser(ctx, imapConn, user.GluonKey()) - if err != nil { - return fmt.Errorf("failed to add IMAP user: %w", err) - } - - if err := user.SetGluonID(addrID, gluonID); err != nil { - return fmt.Errorf("failed to set IMAP user ID: %w", err) - } - - log.WithField("gluonID", gluonID).Info("Created new IMAP user") - } - } - - // Trigger a sync for the user, if needed. - user.TriggerSync() - - return nil + return bridge.serverManager.AddIMAPUser(ctx, user) } // removeIMAPUser disconnects the given user from gluon, optionally also removing its files. func (bridge *Bridge) removeIMAPUser(ctx context.Context, user *user.User, withData bool) error { - if bridge.imapServer == nil { - return fmt.Errorf("no imap server instance running") - } - - logrus.WithFields(logrus.Fields{ - "userID": user.ID(), - "withData": withData, - }).Debug("Removing IMAP user") - - for addrID, gluonID := range user.GetGluonIDs() { - if err := bridge.imapServer.RemoveUser(ctx, gluonID, withData); err != nil { - return fmt.Errorf("failed to remove IMAP user: %w", err) - } - - if withData { - if err := user.RemoveGluonID(addrID, gluonID); err != nil { - return fmt.Errorf("failed to remove IMAP user ID: %w", err) - } - } - } - - return nil + return bridge.serverManager.RemoveIMAPUser(ctx, user, withData) } func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) { @@ -267,14 +84,6 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) { } } -func getGluonDir(encVault *vault.Vault) (string, error) { - if err := os.MkdirAll(encVault.GetGluonCacheDir(), 0o700); err != nil { - return "", fmt.Errorf("failed to create gluon dir: %w", err) - } - - return encVault.GetGluonCacheDir(), nil -} - func ApplyGluonCachePathSuffix(basePath string) string { return filepath.Join(basePath, "backend", "store") } diff --git a/internal/bridge/server_manager.go b/internal/bridge/server_manager.go new file mode 100644 index 00000000..f1aecf4a --- /dev/null +++ b/internal/bridge/server_manager.go @@ -0,0 +1,620 @@ +// Copyright (c) 2023 Proton AG +// +// This file is part of Proton Mail Bridge. +// +// Proton Mail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Proton Mail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Proton Mail Bridge. If not, see . + +package bridge + +import ( + "context" + "fmt" + "net" + "path/filepath" + + "github.com/ProtonMail/gluon" + "github.com/ProtonMail/gluon/connector" + "github.com/ProtonMail/gluon/logging" + "github.com/ProtonMail/proton-bridge/v3/cpc" + "github.com/ProtonMail/proton-bridge/v3/internal/events" + "github.com/ProtonMail/proton-bridge/v3/internal/safe" + "github.com/ProtonMail/proton-bridge/v3/internal/user" + "github.com/emersion/go-smtp" + "github.com/sirupsen/logrus" +) + +// ServerManager manages the IMAP & SMTP servers and their listeners. +type ServerManager struct { + requests *cpc.CPC + + imapServer *gluon.Server + imapListener net.Listener + + smtpServer *smtp.Server + smtpListener net.Listener +} + +func newServerManager() *ServerManager { + return &ServerManager{ + requests: cpc.NewCPC(), + } +} + +func (sm *ServerManager) Init(bridge *Bridge) error { + imapServer, err := createIMAPServer(bridge) + if err != nil { + return err + } + + smtpServer := createSMTPServer(bridge) + + sm.imapServer = imapServer + sm.smtpServer = smtpServer + + bridge.tasks.Once(func(ctx context.Context) { + logging.DoAnnotated(ctx, func(ctx context.Context) { + sm.run(ctx, bridge) + }, logging.Labels{ + "service": "server-manager", + }) + }) + + return nil +} + +func (sm *ServerManager) CloseServers(ctx context.Context) error { + defer sm.requests.Close() + _, err := sm.requests.SendWithReply(ctx, &smRequestClose{}) + + return err +} + +func (sm *ServerManager) RestartIMAP(ctx context.Context) error { + _, err := sm.requests.SendWithReply(ctx, &smRequestRestartIMAP{}) + + return err +} + +func (sm *ServerManager) RestartSMTP(ctx context.Context) error { + _, err := sm.requests.SendWithReply(ctx, &smRequestRestartSMTP{}) + + return err +} + +func (sm *ServerManager) AddIMAPUser(ctx context.Context, user *user.User) error { + _, err := sm.requests.SendWithReply(ctx, &smRequestAddIMAPUser{user: user}) + + return err +} + +func (sm *ServerManager) RemoveIMAPUser(ctx context.Context, user *user.User, withData bool) error { + _, err := sm.requests.SendWithReply(ctx, &smRequestRemoveIMAPUser{ + user: user, + withData: withData, + }) + + return err +} + +func (sm *ServerManager) SetGluonDir(ctx context.Context, gluonDir string) error { + _, err := sm.requests.SendWithReply(ctx, &smRequestSetGluonDir{ + dir: gluonDir, + }) + + return err +} + +func (sm *ServerManager) AddGluonUser(ctx context.Context, conn connector.Connector, passphrase []byte) (string, error) { + reply, err := cpc.SendWithReplyType[string](ctx, sm.requests, &smRequestAddGluonUser{ + conn: conn, + passphrase: passphrase, + }) + + return reply, err +} + +func (sm *ServerManager) RemoveGluonUser(ctx context.Context, gluonID string) error { + _, err := sm.requests.SendWithReply(ctx, &smRequestRemoveGluonUser{ + userID: gluonID, + }) + + return err +} + +func (sm *ServerManager) run(ctx context.Context, bridge *Bridge) { + eventCh, cancel := bridge.GetEvents() + defer cancel() + + for { + select { + case <-ctx.Done(): + sm.handleClose(ctx, bridge) + return + + case evt := <-eventCh: + switch evt.(type) { + case events.ConnStatusDown: + // Handle connect down. + + case events.ConnStatusUp: + // Handle connect up. + + case events.AllUsersLoaded: + if err := sm.serveIMAP(ctx, bridge); err != nil { + logrus.WithError(err).Error("Failed to start IMAP server") + } + + if err := sm.serveSMTP(bridge); err != nil { + logrus.WithError(err).Error("Failed to start SMTP server") + } + } + + case request, ok := <-sm.requests.ReceiveCh(): + if !ok { + return + } + + switch r := request.Value.(type) { + case *smRequestClose: + sm.handleClose(ctx, bridge) + request.SendReply(ctx, nil, nil) + return + + case *smRequestRestartSMTP: + err := sm.restartSMTP(bridge) + request.SendReply(ctx, nil, err) + + case *smRequestRestartIMAP: + err := sm.restartIMAP(ctx, bridge) + request.SendReply(ctx, nil, err) + + case *smRequestAddIMAPUser: + err := sm.handleAddIMAPUser(ctx, r.user) + request.SendReply(ctx, nil, err) + + case *smRequestRemoveIMAPUser: + err := sm.handleRemoveIMAPUser(ctx, r.user, r.withData) + request.SendReply(ctx, nil, err) + + case *smRequestSetGluonDir: + err := sm.handleSetGluonDir(ctx, bridge, r.dir) + request.SendReply(ctx, nil, err) + + case *smRequestAddGluonUser: + id, err := sm.handleAddGluonUser(ctx, r.conn, r.passphrase) + request.SendReply(ctx, id, err) + + case *smRequestRemoveGluonUser: + err := sm.handleRemoveGluonUser(ctx, r.userID) + request.SendReply(ctx, nil, err) + } + } + } +} + +func (sm *ServerManager) handleClose(ctx context.Context, bridge *Bridge) { + // Close the IMAP server. + if err := sm.closeIMAPServer(ctx, bridge); err != nil { + logrus.WithError(err).Error("Failed to close IMAP server") + } + + // Close the SMTP server. + if err := sm.closeSMTPServer(bridge); err != nil { + logrus.WithError(err).Error("Failed to close SMTP server") + } +} + +func (sm *ServerManager) handleAddIMAPUser(ctx context.Context, user *user.User) error { + if sm.imapServer == nil { + return fmt.Errorf("no imap server instance running") + } + + imapConn, err := user.NewIMAPConnectors() + if err != nil { + return fmt.Errorf("failed to create IMAP connectors: %w", err) + } + + for addrID, imapConn := range imapConn { + log := logrus.WithFields(logrus.Fields{ + "userID": user.ID(), + "addrID": addrID, + }) + + if gluonID, ok := user.GetGluonID(addrID); ok { + log.WithField("gluonID", gluonID).Info("Loading existing IMAP user") + + // Load the user, checking whether the DB was newly created. + isNew, err := sm.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey()) + if err != nil { + return fmt.Errorf("failed to load IMAP user: %w", err) + } + + if isNew { + // If the DB was newly created, clear the sync status; gluon's DB was not found. + logrus.Warn("IMAP user DB was newly created, clearing sync status") + + // Remove the user from IMAP so we can clear the sync status. + if err := sm.imapServer.RemoveUser(ctx, gluonID, false); err != nil { + return fmt.Errorf("failed to remove IMAP user: %w", err) + } + + // Clear the sync status -- we need to resync all messages. + if err := user.ClearSyncStatus(); err != nil { + return fmt.Errorf("failed to clear sync status: %w", err) + } + + // Add the user back to the IMAP server. + if isNew, err := sm.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey()); err != nil { + return fmt.Errorf("failed to add IMAP user: %w", err) + } else if isNew { + panic("IMAP user should already have a database") + } + } else if status := user.GetSyncStatus(); !status.HasLabels { + // Otherwise, the DB already exists -- if the labels are not yet synced, we need to re-create the DB. + if err := sm.imapServer.RemoveUser(ctx, gluonID, true); err != nil { + return fmt.Errorf("failed to remove old IMAP user: %w", err) + } + + if err := user.RemoveGluonID(addrID, gluonID); err != nil { + return fmt.Errorf("failed to remove old IMAP user ID: %w", err) + } + + gluonID, err := sm.imapServer.AddUser(ctx, imapConn, user.GluonKey()) + if err != nil { + return fmt.Errorf("failed to add IMAP user: %w", err) + } + + if err := user.SetGluonID(addrID, gluonID); err != nil { + return fmt.Errorf("failed to set IMAP user ID: %w", err) + } + + log.WithField("gluonID", gluonID).Info("Re-created IMAP user") + } + } else { + log.Info("Creating new IMAP user") + + gluonID, err := sm.imapServer.AddUser(ctx, imapConn, user.GluonKey()) + if err != nil { + return fmt.Errorf("failed to add IMAP user: %w", err) + } + + if err := user.SetGluonID(addrID, gluonID); err != nil { + return fmt.Errorf("failed to set IMAP user ID: %w", err) + } + + log.WithField("gluonID", gluonID).Info("Created new IMAP user") + } + } + + // Trigger a sync for the user, if needed. + user.TriggerSync() + + return nil +} + +func (sm *ServerManager) handleRemoveIMAPUser(ctx context.Context, user *user.User, withData bool) error { + if sm.imapServer == nil { + return fmt.Errorf("no imap server instance running") + } + + logrus.WithFields(logrus.Fields{ + "userID": user.ID(), + "withData": withData, + }).Debug("Removing IMAP user") + + for addrID, gluonID := range user.GetGluonIDs() { + if err := sm.imapServer.RemoveUser(ctx, gluonID, withData); err != nil { + return fmt.Errorf("failed to remove IMAP user: %w", err) + } + + if withData { + if err := user.RemoveGluonID(addrID, gluonID); err != nil { + return fmt.Errorf("failed to remove IMAP user ID: %w", err) + } + } + } + + return nil +} + +func createIMAPServer(bridge *Bridge) (*gluon.Server, error) { + gluonDataDir, err := bridge.GetGluonDataDir() + if err != nil { + return nil, fmt.Errorf("failed to get Gluon Database directory: %w", err) + } + + return newIMAPServer( + bridge.vault.GetGluonCacheDir(), + gluonDataDir, + bridge.curVersion, + bridge.tlsConfig, + bridge.reporter, + bridge.logIMAPClient, + bridge.logIMAPServer, + bridge.imapEventCh, + bridge.tasks, + bridge.uidValidityGenerator, + bridge.panicHandler, + ) +} + +func createSMTPServer(bridge *Bridge) *smtp.Server { + return newSMTPServer(bridge, bridge.tlsConfig, bridge.logSMTP) +} + +func (sm *ServerManager) closeSMTPServer(bridge *Bridge) error { + // We close the listener ourselves even though it's also closed by smtpServer.Close(). + // This is because smtpServer.Serve() is called in a separate goroutine and might be executed + // after we've already closed the server. However, go-smtp has a bug; it blocks on the listener + // even after the server has been closed. So we close the listener ourselves to unblock it. + logrus.Info("Closing SMTP server") + + if sm.smtpListener != nil { + if err := sm.smtpListener.Close(); err != nil { + return fmt.Errorf("failed to close SMTP listener: %w", err) + } + } + + if err := sm.smtpServer.Close(); err != nil { + logrus.WithError(err).Debug("Failed to close SMTP server (expected -- we close the listener ourselves)") + } + + bridge.publish(events.SMTPServerStopped{}) + + return nil +} + +func (sm *ServerManager) closeIMAPServer(ctx context.Context, bridge *Bridge) error { + logrus.Info("Closing IMAP server") + + if sm.imapServer != nil { + if err := sm.imapServer.Close(ctx); err != nil { + return fmt.Errorf("failed to close IMAP server: %w", err) + } + + sm.imapServer = nil + } + + if sm.imapListener != nil { + if err := sm.imapListener.Close(); err != nil { + return fmt.Errorf("failed to close IMAP listener: %w", err) + } + + sm.imapListener = nil + } + + bridge.publish(events.IMAPServerStopped{}) + + return nil +} + +func (sm *ServerManager) restartIMAP(ctx context.Context, bridge *Bridge) error { + logrus.Info("Restarting IMAP server") + + if sm.imapListener != nil { + if err := sm.imapListener.Close(); err != nil { + return fmt.Errorf("failed to close IMAP listener: %w", err) + } + + sm.imapListener = nil + + bridge.publish(events.IMAPServerStopped{}) + } + + return sm.serveIMAP(ctx, bridge) +} + +func (sm *ServerManager) restartSMTP(bridge *Bridge) error { + logrus.Info("Restarting SMTP server") + + if err := sm.closeSMTPServer(bridge); err != nil { + return fmt.Errorf("failed to close SMTP: %w", err) + } + + bridge.publish(events.SMTPServerStopped{}) + + sm.smtpServer = newSMTPServer(bridge, bridge.tlsConfig, bridge.logSMTP) + + return sm.serveSMTP(bridge) +} + +func (sm *ServerManager) serveSMTP(bridge *Bridge) error { + port, err := func() (int, error) { + logrus.WithFields(logrus.Fields{ + "port": bridge.vault.GetSMTPPort(), + "ssl": bridge.vault.GetSMTPSSL(), + }).Info("Starting SMTP server") + + smtpListener, err := newListener(bridge.vault.GetSMTPPort(), bridge.vault.GetSMTPSSL(), bridge.tlsConfig) + if err != nil { + return 0, fmt.Errorf("failed to create SMTP listener: %w", err) + } + + sm.smtpListener = smtpListener + + bridge.tasks.Once(func(context.Context) { + if err := sm.smtpServer.Serve(smtpListener); err != nil { + logrus.WithError(err).Info("SMTP server stopped") + } + }) + + if err := bridge.vault.SetSMTPPort(getPort(smtpListener.Addr())); err != nil { + return 0, fmt.Errorf("failed to store SMTP port in vault: %w", err) + } + + return getPort(smtpListener.Addr()), nil + }() + + if err != nil { + bridge.publish(events.SMTPServerError{ + Error: err, + }) + + return err + } + + bridge.publish(events.SMTPServerReady{ + Port: port, + }) + + return nil +} + +func (sm *ServerManager) serveIMAP(ctx context.Context, bridge *Bridge) error { + port, err := func() (int, error) { + if sm.imapServer == nil { + return 0, fmt.Errorf("no IMAP server instance running") + } + + logrus.WithFields(logrus.Fields{ + "port": bridge.vault.GetIMAPPort(), + "ssl": bridge.vault.GetIMAPSSL(), + }).Info("Starting IMAP server") + + imapListener, err := newListener(bridge.vault.GetIMAPPort(), bridge.vault.GetIMAPSSL(), bridge.tlsConfig) + if err != nil { + return 0, fmt.Errorf("failed to create IMAP listener: %w", err) + } + + sm.imapListener = imapListener + + if err := sm.imapServer.Serve(ctx, sm.imapListener); err != nil { + return 0, fmt.Errorf("failed to serve IMAP: %w", err) + } + + if err := bridge.vault.SetIMAPPort(getPort(imapListener.Addr())); err != nil { + return 0, fmt.Errorf("failed to store IMAP port in vault: %w", err) + } + + return getPort(imapListener.Addr()), nil + }() + + if err != nil { + bridge.publish(events.IMAPServerError{ + Error: err, + }) + + return err + } + + bridge.publish(events.IMAPServerReady{ + Port: port, + }) + + return nil +} + +func (sm *ServerManager) handleSetGluonDir(ctx context.Context, bridge *Bridge, newGluonDir string) error { + return safe.RLockRet(func() error { + currentGluonDir := bridge.GetGluonCacheDir() + newGluonDir = filepath.Join(newGluonDir, "gluon") + if newGluonDir == currentGluonDir { + return fmt.Errorf("new gluon dir is the same as the old one") + } + + if err := sm.closeIMAPServer(context.Background(), bridge); err != nil { + return fmt.Errorf("failed to close IMAP: %w", err) + } + + if err := bridge.moveGluonCacheDir(currentGluonDir, newGluonDir); err != nil { + logrus.WithError(err).Error("failed to move GluonCacheDir") + + if err := bridge.vault.SetGluonDir(currentGluonDir); err != nil { + return fmt.Errorf("failed to revert GluonCacheDir: %w", err) + } + } + + bridge.heartbeat.SetCacheLocation(newGluonDir) + + gluonDataDir, err := bridge.GetGluonDataDir() + if err != nil { + return fmt.Errorf("failed to get Gluon Database directory: %w", err) + } + + imapServer, err := newIMAPServer( + bridge.vault.GetGluonCacheDir(), + gluonDataDir, + bridge.curVersion, + bridge.tlsConfig, + bridge.reporter, + bridge.logIMAPClient, + bridge.logIMAPServer, + bridge.imapEventCh, + bridge.tasks, + bridge.uidValidityGenerator, + bridge.panicHandler, + ) + if err != nil { + return fmt.Errorf("failed to create new IMAP server: %w", err) + } + + sm.imapServer = imapServer + + for _, bridgeUser := range bridge.users { + if err := sm.handleAddIMAPUser(ctx, bridgeUser); err != nil { + return fmt.Errorf("failed to add users to new IMAP server: %w", err) + } + } + + if err := sm.serveIMAP(ctx, bridge); err != nil { + return fmt.Errorf("failed to serve IMAP: %w", err) + } + + return nil + }, bridge.usersLock) +} + +func (sm *ServerManager) handleAddGluonUser(ctx context.Context, conn connector.Connector, passphrase []byte) (string, error) { + if sm.imapServer == nil { + return "", fmt.Errorf("no imap server instance running") + } + + return sm.imapServer.AddUser(ctx, conn, passphrase) +} + +func (sm *ServerManager) handleRemoveGluonUser(ctx context.Context, userID string) error { + if sm.imapServer == nil { + return fmt.Errorf("no imap server instance running") + } + + return sm.imapServer.RemoveUser(ctx, userID, true) +} + +type smRequestClose struct{} + +type smRequestRestartIMAP struct{} + +type smRequestRestartSMTP struct{} + +type smRequestAddIMAPUser struct { + user *user.User +} + +type smRequestRemoveIMAPUser struct { + user *user.User + withData bool +} + +type smRequestSetGluonDir struct { + dir string +} + +type smRequestAddGluonUser struct { + conn connector.Connector + passphrase []byte +} + +type smRequestRemoveGluonUser struct { + userID string +} diff --git a/internal/bridge/settings.go b/internal/bridge/settings.go index dd727316..dbbadecb 100644 --- a/internal/bridge/settings.go +++ b/internal/bridge/settings.go @@ -22,7 +22,6 @@ import ( "fmt" "net" "os" - "path/filepath" "github.com/Masterminds/semver/v3" "github.com/ProtonMail/proton-bridge/v3/internal/safe" @@ -55,7 +54,7 @@ func (bridge *Bridge) GetIMAPPort() int { return bridge.vault.GetIMAPPort() } -func (bridge *Bridge) SetIMAPPort(newPort int) error { +func (bridge *Bridge) SetIMAPPort(ctx context.Context, newPort int) error { if newPort == bridge.vault.GetIMAPPort() { return nil } @@ -66,14 +65,14 @@ func (bridge *Bridge) SetIMAPPort(newPort int) error { bridge.heartbeat.SetIMAPPort(newPort) - return bridge.restartIMAP() + return bridge.restartIMAP(ctx) } func (bridge *Bridge) GetIMAPSSL() bool { return bridge.vault.GetIMAPSSL() } -func (bridge *Bridge) SetIMAPSSL(newSSL bool) error { +func (bridge *Bridge) SetIMAPSSL(ctx context.Context, newSSL bool) error { if newSSL == bridge.vault.GetIMAPSSL() { return nil } @@ -84,14 +83,14 @@ func (bridge *Bridge) SetIMAPSSL(newSSL bool) error { bridge.heartbeat.SetIMAPConnectionMode(newSSL) - return bridge.restartIMAP() + return bridge.restartIMAP(ctx) } func (bridge *Bridge) GetSMTPPort() int { return bridge.vault.GetSMTPPort() } -func (bridge *Bridge) SetSMTPPort(newPort int) error { +func (bridge *Bridge) SetSMTPPort(ctx context.Context, newPort int) error { if newPort == bridge.vault.GetSMTPPort() { return nil } @@ -102,14 +101,14 @@ func (bridge *Bridge) SetSMTPPort(newPort int) error { bridge.heartbeat.SetSMTPPort(newPort) - return bridge.restartSMTP() + return bridge.restartSMTP(ctx) } func (bridge *Bridge) GetSMTPSSL() bool { return bridge.vault.GetSMTPSSL() } -func (bridge *Bridge) SetSMTPSSL(newSSL bool) error { +func (bridge *Bridge) SetSMTPSSL(ctx context.Context, newSSL bool) error { if newSSL == bridge.vault.GetSMTPSSL() { return nil } @@ -120,7 +119,7 @@ func (bridge *Bridge) SetSMTPSSL(newSSL bool) error { bridge.heartbeat.SetSMTPConnectionMode(newSSL) - return bridge.restartSMTP() + return bridge.restartSMTP(ctx) } func (bridge *Bridge) GetGluonCacheDir() string { @@ -132,63 +131,7 @@ func (bridge *Bridge) GetGluonDataDir() (string, error) { } func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error { - return safe.RLockRet(func() error { - currentGluonDir := bridge.GetGluonCacheDir() - newGluonDir = filepath.Join(newGluonDir, "gluon") - if newGluonDir == currentGluonDir { - return fmt.Errorf("new gluon dir is the same as the old one") - } - - if err := bridge.closeIMAP(context.Background()); err != nil { - return fmt.Errorf("failed to close IMAP: %w", err) - } - - if err := bridge.moveGluonCacheDir(currentGluonDir, newGluonDir); err != nil { - logrus.WithError(err).Error("failed to move GluonCacheDir") - - if err := bridge.vault.SetGluonDir(currentGluonDir); err != nil { - return fmt.Errorf("failed to revert GluonCacheDir: %w", err) - } - } - - bridge.heartbeat.SetCacheLocation(newGluonDir) - - gluonDataDir, err := bridge.GetGluonDataDir() - if err != nil { - return fmt.Errorf("failed to get Gluon Database directory: %w", err) - } - - imapServer, err := newIMAPServer( - bridge.vault.GetGluonCacheDir(), - gluonDataDir, - bridge.curVersion, - bridge.tlsConfig, - bridge.reporter, - bridge.logIMAPClient, - bridge.logIMAPServer, - bridge.imapEventCh, - bridge.tasks, - bridge.uidValidityGenerator, - bridge.panicHandler, - ) - if err != nil { - return fmt.Errorf("failed to create new IMAP server: %w", err) - } - - bridge.imapServer = imapServer - - for _, user := range bridge.users { - if err := bridge.addIMAPUser(ctx, user); err != nil { - return fmt.Errorf("failed to add users to new IMAP server: %w", err) - } - } - - if err := bridge.serveIMAP(); err != nil { - return fmt.Errorf("failed to serve IMAP: %w", err) - } - - return nil - }, bridge.usersLock) + return bridge.serverManager.SetGluonDir(ctx, newGluonDir) } func (bridge *Bridge) moveGluonCacheDir(oldGluonDir, newGluonDir string) error { diff --git a/internal/bridge/settings_test.go b/internal/bridge/settings_test.go index 6a48c827..f10d1932 100644 --- a/internal/bridge/settings_test.go +++ b/internal/bridge/settings_test.go @@ -57,7 +57,7 @@ func TestBridge_Settings_IMAPPort(t *testing.T) { curPort := bridge.GetIMAPPort() // Set the port to 1144. - require.NoError(t, bridge.SetIMAPPort(1144)) + require.NoError(t, bridge.SetIMAPPort(ctx, 1144)) // Get the new setting. require.Equal(t, 1144, bridge.GetIMAPPort()) @@ -75,7 +75,7 @@ func TestBridge_Settings_IMAPSSL(t *testing.T) { require.False(t, bridge.GetIMAPSSL()) // Enable IMAP SSL. - require.NoError(t, bridge.SetIMAPSSL(true)) + require.NoError(t, bridge.SetIMAPSSL(ctx, true)) // Get the new setting. require.True(t, bridge.GetIMAPSSL()) @@ -89,7 +89,7 @@ func TestBridge_Settings_SMTPPort(t *testing.T) { curPort := bridge.GetSMTPPort() // Set the port to 1024. - require.NoError(t, bridge.SetSMTPPort(1024)) + require.NoError(t, bridge.SetSMTPPort(ctx, 1024)) // Get the new setting. require.Equal(t, 1024, bridge.GetSMTPPort()) @@ -107,7 +107,7 @@ func TestBridge_Settings_SMTPSSL(t *testing.T) { require.False(t, bridge.GetSMTPSSL()) // Enable SMTP SSL. - require.NoError(t, bridge.SetSMTPSSL(true)) + require.NoError(t, bridge.SetSMTPSSL(ctx, true)) // Get the new setting. require.True(t, bridge.GetSMTPSSL()) diff --git a/internal/bridge/smtp.go b/internal/bridge/smtp.go index 9b88b811..3aeb4f8d 100644 --- a/internal/bridge/smtp.go +++ b/internal/bridge/smtp.go @@ -20,93 +20,16 @@ package bridge import ( "context" "crypto/tls" - "fmt" - - "github.com/ProtonMail/proton-bridge/v3/internal/events" - "github.com/ProtonMail/proton-bridge/v3/internal/logging" "github.com/ProtonMail/proton-bridge/v3/internal/constants" + "github.com/ProtonMail/proton-bridge/v3/internal/logging" "github.com/emersion/go-sasl" "github.com/emersion/go-smtp" "github.com/sirupsen/logrus" ) -func (bridge *Bridge) serveSMTP() error { - port, err := func() (int, error) { - logrus.WithFields(logrus.Fields{ - "port": bridge.vault.GetSMTPPort(), - "ssl": bridge.vault.GetSMTPSSL(), - }).Info("Starting SMTP server") - - smtpListener, err := newListener(bridge.vault.GetSMTPPort(), bridge.vault.GetSMTPSSL(), bridge.tlsConfig) - if err != nil { - return 0, fmt.Errorf("failed to create SMTP listener: %w", err) - } - - bridge.smtpListener = smtpListener - - bridge.tasks.Once(func(context.Context) { - if err := bridge.smtpServer.Serve(smtpListener); err != nil { - logrus.WithError(err).Info("SMTP server stopped") - } - }) - - if err := bridge.vault.SetSMTPPort(getPort(smtpListener.Addr())); err != nil { - return 0, fmt.Errorf("failed to store SMTP port in vault: %w", err) - } - - return getPort(smtpListener.Addr()), nil - }() - - if err != nil { - bridge.publish(events.SMTPServerError{ - Error: err, - }) - - return err - } - - bridge.publish(events.SMTPServerReady{ - Port: port, - }) - - return nil -} - -func (bridge *Bridge) restartSMTP() error { - logrus.Info("Restarting SMTP server") - - if err := bridge.closeSMTP(); err != nil { - return fmt.Errorf("failed to close SMTP: %w", err) - } - - bridge.publish(events.SMTPServerStopped{}) - - bridge.smtpServer = newSMTPServer(bridge, bridge.tlsConfig, bridge.logSMTP) - - return bridge.serveSMTP() -} - -// We close the listener ourselves even though it's also closed by smtpServer.Close(). -// This is because smtpServer.Serve() is called in a separate goroutine and might be executed -// after we've already closed the server. However, go-smtp has a bug; it blocks on the listener -// even after the server has been closed. So we close the listener ourselves to unblock it. -func (bridge *Bridge) closeSMTP() error { - logrus.Info("Closing SMTP server") - - if bridge.smtpListener != nil { - if err := bridge.smtpListener.Close(); err != nil { - return fmt.Errorf("failed to close SMTP listener: %w", err) - } - } - - if err := bridge.smtpServer.Close(); err != nil { - logrus.WithError(err).Debug("Failed to close SMTP server (expected -- we close the listener ourselves)") - } - - bridge.publish(events.SMTPServerStopped{}) - - return nil +func (bridge *Bridge) restartSMTP(ctx context.Context) error { + return bridge.serverManager.RestartSMTP(ctx) } func newSMTPServer(bridge *Bridge, tlsConfig *tls.Config, logSMTP bool) *smtp.Server { diff --git a/internal/bridge/user_events.go b/internal/bridge/user_events.go index d5a310df..8aee9e42 100644 --- a/internal/bridge/user_events.go +++ b/internal/bridge/user_events.go @@ -75,11 +75,7 @@ func (bridge *Bridge) handleUserAddressCreated(ctx context.Context, user *user.U return nil } - if bridge.imapServer == nil { - return fmt.Errorf("no imap server instance running") - } - - gluonID, err := bridge.imapServer.AddUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey()) + gluonID, err := bridge.serverManager.AddGluonUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey()) if err != nil { return fmt.Errorf("failed to add user to IMAP server: %w", err) } @@ -96,7 +92,7 @@ func (bridge *Bridge) handleUserAddressEnabled(ctx context.Context, user *user.U return nil } - gluonID, err := bridge.imapServer.AddUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey()) + gluonID, err := bridge.serverManager.AddGluonUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey()) if err != nil { return fmt.Errorf("failed to add user to IMAP server: %w", err) } @@ -118,7 +114,7 @@ func (bridge *Bridge) handleUserAddressDisabled(ctx context.Context, user *user. return fmt.Errorf("gluon ID not found for address %s", event.AddressID) } - if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil { + if err := bridge.serverManager.RemoveGluonUser(ctx, gluonID); err != nil { return fmt.Errorf("failed to remove user from IMAP server: %w", err) } @@ -134,16 +130,12 @@ func (bridge *Bridge) handleUserAddressDeleted(ctx context.Context, user *user.U return nil } - if bridge.imapServer == nil { - return fmt.Errorf("no imap server instance running") - } - gluonID, ok := user.GetGluonID(event.AddressID) if !ok { return fmt.Errorf("gluon ID not found for address %s", event.AddressID) } - if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil { + if err := bridge.serverManager.handleRemoveGluonUser(ctx, gluonID); err != nil { return fmt.Errorf("failed to remove user from IMAP server: %w", err) } diff --git a/internal/frontend/cli/accounts.go b/internal/frontend/cli/accounts.go index fdef4b17..cabb7413 100644 --- a/internal/frontend/cli/accounts.go +++ b/internal/frontend/cli/accounts.go @@ -297,7 +297,7 @@ func (f *frontendCLI) configureAppleMail(c *ishell.Context) { return } - if err := f.bridge.ConfigureAppleMail(user.UserID, user.Addresses[0]); err != nil { + if err := f.bridge.ConfigureAppleMail(context.Background(), user.UserID, user.Addresses[0]); err != nil { f.printAndLogError(err) return } diff --git a/internal/frontend/cli/system.go b/internal/frontend/cli/system.go index 3ea41dca..d163db1f 100644 --- a/internal/frontend/cli/system.go +++ b/internal/frontend/cli/system.go @@ -61,7 +61,7 @@ func (f *frontendCLI) changeIMAPSecurity(_ *ishell.Context) { msg := fmt.Sprintf("Are you sure you want to change IMAP setting to %q", newSecurity) if f.yesNoQuestion(msg) { - if err := f.bridge.SetIMAPSSL(!f.bridge.GetIMAPSSL()); err != nil { + if err := f.bridge.SetIMAPSSL(context.Background(), !f.bridge.GetIMAPSSL()); err != nil { f.printAndLogError(err) return } @@ -80,7 +80,7 @@ func (f *frontendCLI) changeSMTPSecurity(_ *ishell.Context) { msg := fmt.Sprintf("Are you sure you want to change SMTP setting to %q", newSecurity) if f.yesNoQuestion(msg) { - if err := f.bridge.SetSMTPSSL(!f.bridge.GetSMTPSSL()); err != nil { + if err := f.bridge.SetSMTPSSL(context.Background(), !f.bridge.GetSMTPSSL()); err != nil { f.printAndLogError(err) return } @@ -103,7 +103,7 @@ func (f *frontendCLI) changeIMAPPort(c *ishell.Context) { return } - if err := f.bridge.SetIMAPPort(newIMAPPortInt); err != nil { + if err := f.bridge.SetIMAPPort(context.Background(), newIMAPPortInt); err != nil { f.printAndLogError(err) return } @@ -125,7 +125,7 @@ func (f *frontendCLI) changeSMTPPort(c *ishell.Context) { return } - if err := f.bridge.SetSMTPPort(newSMTPPortInt); err != nil { + if err := f.bridge.SetSMTPPort(context.Background(), newSMTPPortInt); err != nil { f.printAndLogError(err) return } diff --git a/internal/frontend/grpc/service_methods.go b/internal/frontend/grpc/service_methods.go index 49e86b1f..941d0a6f 100644 --- a/internal/frontend/grpc/service_methods.go +++ b/internal/frontend/grpc/service_methods.go @@ -668,7 +668,7 @@ func (s *Service) MailServerSettings(_ context.Context, _ *emptypb.Empty) (*Imap }, nil } -func (s *Service) SetMailServerSettings(_ context.Context, settings *ImapSmtpSettings) (*emptypb.Empty, error) { +func (s *Service) SetMailServerSettings(ctx context.Context, settings *ImapSmtpSettings) (*emptypb.Empty, error) { s.log. WithField("ImapPort", settings.ImapPort). WithField("SmtpPort", settings.SmtpPort). @@ -682,28 +682,28 @@ func (s *Service) SetMailServerSettings(_ context.Context, settings *ImapSmtpSet defer func() { _ = s.SendEvent(NewChangeMailServerSettingsFinishedEvent()) }() if s.bridge.GetIMAPSSL() != settings.UseSSLForImap { - if err := s.bridge.SetIMAPSSL(settings.UseSSLForImap); err != nil { + if err := s.bridge.SetIMAPSSL(ctx, settings.UseSSLForImap); err != nil { s.log.WithError(err).Error("Failed to set IMAP SSL") _ = s.SendEvent(NewMailServerSettingsErrorEvent(MailServerSettingsErrorType_IMAP_CONNECTION_MODE_CHANGE_ERROR)) } } if s.bridge.GetSMTPSSL() != settings.UseSSLForSmtp { - if err := s.bridge.SetSMTPSSL(settings.UseSSLForSmtp); err != nil { + if err := s.bridge.SetSMTPSSL(ctx, settings.UseSSLForSmtp); err != nil { s.log.WithError(err).Error("Failed to set SMTP SSL") _ = s.SendEvent(NewMailServerSettingsErrorEvent(MailServerSettingsErrorType_SMTP_CONNECTION_MODE_CHANGE_ERROR)) } } if s.bridge.GetIMAPPort() != int(settings.ImapPort) { - if err := s.bridge.SetIMAPPort(int(settings.ImapPort)); err != nil { + if err := s.bridge.SetIMAPPort(ctx, int(settings.ImapPort)); err != nil { s.log.WithError(err).Error("Failed to set IMAP port") _ = s.SendEvent(NewMailServerSettingsErrorEvent(MailServerSettingsErrorType_IMAP_PORT_CHANGE_ERROR)) } } if s.bridge.GetSMTPPort() != int(settings.SmtpPort) { - if err := s.bridge.SetSMTPPort(int(settings.SmtpPort)); err != nil { + if err := s.bridge.SetSMTPPort(ctx, int(settings.SmtpPort)); err != nil { s.log.WithError(err).Error("Failed to set SMTP port") _ = s.SendEvent(NewMailServerSettingsErrorEvent(MailServerSettingsErrorType_SMTP_PORT_CHANGE_ERROR)) } diff --git a/internal/frontend/grpc/service_user.go b/internal/frontend/grpc/service_user.go index 91473c60..248acc4e 100644 --- a/internal/frontend/grpc/service_user.go +++ b/internal/frontend/grpc/service_user.go @@ -147,12 +147,12 @@ func (s *Service) RemoveUser(_ context.Context, userID *wrapperspb.StringValue) return &emptypb.Empty{}, nil } -func (s *Service) ConfigureUserAppleMail(_ context.Context, request *ConfigureAppleMailRequest) (*emptypb.Empty, error) { +func (s *Service) ConfigureUserAppleMail(ctx context.Context, request *ConfigureAppleMailRequest) (*emptypb.Empty, error) { s.log.WithField("UserID", request.UserID).WithField("Address", request.Address).Debug("ConfigureUserAppleMail") sslWasEnabled := s.bridge.GetSMTPSSL() - if err := s.bridge.ConfigureAppleMail(request.UserID, request.Address); err != nil { + if err := s.bridge.ConfigureAppleMail(ctx, request.UserID, request.Address); err != nil { s.log.WithField("userID", request.UserID).Error("Cannot configure AppleMail for user") return nil, status.Error(codes.Internal, "Apple Mail config failed") } diff --git a/tests/bridge_test.go b/tests/bridge_test.go index 7d3edd14..fd5f2324 100644 --- a/tests/bridge_test.go +++ b/tests/bridge_test.go @@ -60,11 +60,11 @@ func (s *scenario) theAPIRequiresBridgeVersion(version string) error { } func (s *scenario) theUserChangesTheIMAPPortTo(port int) error { - return s.t.bridge.SetIMAPPort(port) + return s.t.bridge.SetIMAPPort(context.Background(), port) } func (s *scenario) theUserChangesTheSMTPPortTo(port int) error { - return s.t.bridge.SetSMTPPort(port) + return s.t.bridge.SetSMTPPort(context.Background(), port) } func (s *scenario) theUserSetsTheAddressModeOfUserTo(user, mode string) error { @@ -144,11 +144,11 @@ func (s *scenario) theUserHasEnabledAlternativeRouting() error { } func (s *scenario) theUserSetIMAPModeToSSL() error { - return s.t.bridge.SetIMAPSSL(true) + return s.t.bridge.SetIMAPSSL(context.Background(), true) } func (s *scenario) theUserSetSMTPModeToSSL() error { - return s.t.bridge.SetSMTPSSL(true) + return s.t.bridge.SetSMTPSSL(context.Background(), true) } func (s *scenario) theUserReportsABug() error { From 4b5edd62d070470c3cf4be87c7f999952454195b Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Wed, 10 May 2023 10:05:47 +0200 Subject: [PATCH 22/43] feat(GODT-2585): Only Start IMAP/SMTP once one user is loaded Update ServerManager to follow the new expected behavior. The servers will only be started when one user is active. If all users are logged out or removed from the system, the servers will stop. If the network goes down, the servers will stop and resume once network has been restored. --- internal/bridge/bridge_test.go | 105 ++++++++++++++- internal/bridge/send_test.go | 14 +- internal/bridge/server_manager.go | 140 +++++++++++++++----- internal/bridge/server_manager_test.go | 171 +++++++++++++++++++++++++ internal/bridge/sync_test.go | 18 --- internal/bridge/user_event_test.go | 38 ++++-- tests/ctx_test.go | 2 + tests/user_test.go | 11 ++ 8 files changed, 429 insertions(+), 70 deletions(-) create mode 100644 internal/bridge/server_manager_test.go diff --git a/internal/bridge/bridge_test.go b/internal/bridge/bridge_test.go index 1ac94815..c9529e10 100644 --- a/internal/bridge/bridge_test.go +++ b/internal/bridge/bridge_test.go @@ -173,7 +173,19 @@ func TestBridge_UserAgent(t *testing.T) { func TestBridge_UserAgent_Persistence(t *testing.T) { withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) { + otherPassword := []byte("bar") + otherUser := "foo" + _, _, err := s.CreateUser(otherUser, otherPassword) + require.NoError(t, err) + withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) { + imapWaiter := waitForIMAPServerReady(b) + defer imapWaiter.Done() + + require.NoError(t, getErr(b.LoginFull(ctx, otherUser, otherPassword, nil, nil))) + + imapWaiter.Wait() + currentUserAgent := b.GetCurrentUserAgent() require.Contains(t, currentUserAgent, vault.DefaultUserAgent) @@ -220,7 +232,19 @@ func TestBridge_UserAgentFromIMAPID(t *testing.T) { calls = append(calls, call) }) + otherPassword := []byte("bar") + otherUser := "foo" + _, _, err := s.CreateUser(otherUser, otherPassword) + require.NoError(t, err) + withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) { + imapWaiter := waitForIMAPServerReady(b) + defer imapWaiter.Done() + + require.NoError(t, getErr(b.LoginFull(ctx, otherUser, otherPassword, nil, nil))) + + imapWaiter.Wait() + imapClient, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) require.NoError(t, err) defer func() { _ = imapClient.Logout() }() @@ -592,9 +616,17 @@ func TestBridge_InitGluonDirectory(t *testing.T) { func TestBridge_LoginFailed(t *testing.T) { withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + imapWaiter := waitForIMAPServerReady(bridge) + defer imapWaiter.Done() + failCh, done := chToType[events.Event, events.IMAPLoginFailed](bridge.GetEvents(events.IMAPLoginFailed{})) defer done() + _, err := bridge.LoginFull(ctx, username, password, nil, nil) + require.NoError(t, err) + + imapWaiter.Wait() + imapClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort()))) require.NoError(t, err) @@ -622,6 +654,9 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) { configDir, err := b.GetGluonDataDir() require.NoError(t, err) + imapWaiter := waitForIMAPServerReady(b) + defer imapWaiter.Done() + // Login the user. syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{})) defer done() @@ -655,6 +690,8 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) { require.NoError(t, err) require.True(t, info.State == bridge.Connected) + imapWaiter.Wait() + client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) require.NoError(t, err) require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) @@ -778,6 +815,7 @@ func withBridgeNoMocks( locator bridge.Locator, vaultKey []byte, tests func(*bridge.Bridge), + waitOnServers bool, ) { // Bridge will disable the proxy by default at startup. mocks.ProxyCtl.EXPECT().DisallowProxy() @@ -828,15 +866,18 @@ func withBridgeNoMocks( // Wait for bridge to finish loading users. waitForEvent(t, eventCh, events.AllUsersLoaded{}) - // Wait for bridge to start the IMAP server. - waitForEvent(t, eventCh, events.IMAPServerReady{}) - // Wait for bridge to start the SMTP server. - waitForEvent(t, eventCh, events.SMTPServerReady{}) // Set random IMAP and SMTP ports for the tests. require.NoError(t, bridge.SetIMAPPort(ctx, 0)) require.NoError(t, bridge.SetSMTPPort(ctx, 0)) + if waitOnServers { + // Wait for bridge to start the IMAP server. + waitForEvent(t, eventCh, events.IMAPServerReady{}) + // Wait for bridge to start the SMTP server. + waitForEvent(t, eventCh, events.SMTPServerReady{}) + } + // Close the bridge when done. defer bridge.Close(ctx) @@ -857,7 +898,24 @@ func withBridge( withMocks(t, func(mocks *bridge.Mocks) { withBridgeNoMocks(ctx, t, mocks, apiURL, netCtl, locator, vaultKey, func(bridge *bridge.Bridge) { tests(bridge, mocks) - }) + }, false) + }) +} + +// withBridgeWaitForServers is the same as withBridge, but it will wait until IMAP & SMTP servers are ready. +func withBridgeWaitForServers( + ctx context.Context, + t *testing.T, + apiURL string, + netCtl *proton.NetCtl, + locator bridge.Locator, + vaultKey []byte, + tests func(*bridge.Bridge, *bridge.Mocks), +) { + withMocks(t, func(mocks *bridge.Mocks) { + withBridgeNoMocks(ctx, t, mocks, apiURL, netCtl, locator, vaultKey, func(bridge *bridge.Bridge) { + tests(bridge, mocks) + }, true) }) } @@ -910,3 +968,40 @@ func chToType[In, Out any](inCh <-chan In, done func()) (<-chan Out, func()) { return outCh, done } + +type eventWaiter struct { + evtCh <-chan events.Event + cancel func() +} + +func (e *eventWaiter) Done() { + e.cancel() +} + +func (e *eventWaiter) Wait() { + <-e.evtCh +} + +func waitForSMTPServerReady(b *bridge.Bridge) *eventWaiter { + evtCh, cancel := b.GetEvents(events.SMTPServerReady{}) + return &eventWaiter{ + evtCh: evtCh, + cancel: cancel, + } +} + +func waitForIMAPServerReady(b *bridge.Bridge) *eventWaiter { + evtCh, cancel := b.GetEvents(events.IMAPServerReady{}) + return &eventWaiter{ + evtCh: evtCh, + cancel: cancel, + } +} + +func waitForIMAPServerStopped(b *bridge.Bridge) *eventWaiter { + evtCh, cancel := b.GetEvents(events.IMAPServerStopped{}) + return &eventWaiter{ + evtCh: evtCh, + cancel: cancel, + } +} diff --git a/internal/bridge/send_test.go b/internal/bridge/send_test.go index c3b5e1ca..8015e4ea 100644 --- a/internal/bridge/send_test.go +++ b/internal/bridge/send_test.go @@ -46,12 +46,17 @@ func TestBridge_Send(t *testing.T) { require.NoError(t, err) withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) { + smtpWaiter := waitForSMTPServerReady(bridge) + defer smtpWaiter.Done() + senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil) require.NoError(t, err) recipientUserID, err := bridge.LoginFull(ctx, "recipient", password, nil, nil) require.NoError(t, err) + smtpWaiter.Wait() + senderInfo, err := bridge.GetUserInfo(senderUserID) require.NoError(t, err) @@ -135,7 +140,7 @@ func TestBridge_SendDraftFlags(t *testing.T) { }) // Start the bridge. - withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) { + withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) { // Get the sender user info. userInfo, err := bridge.QueryUserInfo(username) require.NoError(t, err) @@ -245,7 +250,7 @@ func TestBridge_SendInvite(t *testing.T) { }) // Start the bridge. - withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) { + withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) { // Get the sender user info. userInfo, err := bridge.QueryUserInfo(username) require.NoError(t, err) @@ -401,6 +406,9 @@ SGVsbG8gd29ybGQK require.NoError(t, err) withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) { + smtpWaiter := waitForSMTPServerReady(bridge) + defer smtpWaiter.Done() + senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil) require.NoError(t, err) @@ -420,6 +428,8 @@ SGVsbG8gd29ybGQK messageMultipartWithoutTextWithTextAttachment, } + smtpWaiter.Wait() + for _, m := range messages { // Dial the server. client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort()))) diff --git a/internal/bridge/server_manager.go b/internal/bridge/server_manager.go index f1aecf4a..acca55c8 100644 --- a/internal/bridge/server_manager.go +++ b/internal/bridge/server_manager.go @@ -43,6 +43,8 @@ type ServerManager struct { smtpServer *smtp.Server smtpListener net.Listener + + loadedUserCount int } func newServerManager() *ServerManager { @@ -145,19 +147,17 @@ func (sm *ServerManager) run(ctx context.Context, bridge *Bridge) { case evt := <-eventCh: switch evt.(type) { case events.ConnStatusDown: - // Handle connect down. + logrus.Info("Server Manager, network down stopping listeners") + if err := sm.closeSMTPServer(bridge); err != nil { + logrus.WithError(err).Error("Failed to close SMTP server") + } + if err := sm.stopIMAPListener(bridge); err != nil { + logrus.WithError(err) + } case events.ConnStatusUp: - // Handle connect up. - - case events.AllUsersLoaded: - if err := sm.serveIMAP(ctx, bridge); err != nil { - logrus.WithError(err).Error("Failed to start IMAP server") - } - - if err := sm.serveSMTP(bridge); err != nil { - logrus.WithError(err).Error("Failed to start SMTP server") - } + logrus.Info("Server Manager, network up starting listeners") + sm.handleLoadedUserCountChange(ctx, bridge) } case request, ok := <-sm.requests.ReceiveCh(): @@ -182,10 +182,18 @@ func (sm *ServerManager) run(ctx context.Context, bridge *Bridge) { case *smRequestAddIMAPUser: err := sm.handleAddIMAPUser(ctx, r.user) request.SendReply(ctx, nil, err) + if err == nil { + sm.loadedUserCount++ + sm.handleLoadedUserCountChange(ctx, bridge) + } case *smRequestRemoveIMAPUser: err := sm.handleRemoveIMAPUser(ctx, r.user, r.withData) request.SendReply(ctx, nil, err) + if err == nil { + sm.loadedUserCount-- + sm.handleLoadedUserCountChange(ctx, bridge) + } case *smRequestSetGluonDir: err := sm.handleSetGluonDir(ctx, bridge, r.dir) @@ -203,6 +211,35 @@ func (sm *ServerManager) run(ctx context.Context, bridge *Bridge) { } } +func (sm *ServerManager) handleLoadedUserCountChange(ctx context.Context, bridge *Bridge) { + logrus.Infof("Validating Listener State %v", sm.loadedUserCount) + if sm.shouldStartServers() { + if sm.imapListener == nil { + if err := sm.serveIMAP(ctx, bridge); err != nil { + logrus.WithError(err).Error("Failed to start IMAP server") + } + } + + if sm.smtpListener == nil { + if err := sm.restartSMTP(bridge); err != nil { + logrus.WithError(err).Error("Failed to start SMTP server") + } + } + } else { + if sm.imapListener != nil { + if err := sm.stopIMAPListener(bridge); err != nil { + logrus.WithError(err).Error("Failed to stop IMAP server") + } + } + + if sm.smtpListener != nil { + if err := sm.closeSMTPServer(bridge); err != nil { + logrus.WithError(err).Error("Failed to stop SMTP server") + } + } + } +} + func (sm *ServerManager) handleClose(ctx context.Context, bridge *Bridge) { // Close the IMAP server. if err := sm.closeIMAPServer(ctx, bridge); err != nil { @@ -358,27 +395,45 @@ func (sm *ServerManager) closeSMTPServer(bridge *Bridge) error { // This is because smtpServer.Serve() is called in a separate goroutine and might be executed // after we've already closed the server. However, go-smtp has a bug; it blocks on the listener // even after the server has been closed. So we close the listener ourselves to unblock it. - logrus.Info("Closing SMTP server") if sm.smtpListener != nil { + logrus.Info("Closing SMTP Listener") if err := sm.smtpListener.Close(); err != nil { return fmt.Errorf("failed to close SMTP listener: %w", err) } + + sm.smtpListener = nil } - if err := sm.smtpServer.Close(); err != nil { - logrus.WithError(err).Debug("Failed to close SMTP server (expected -- we close the listener ourselves)") - } + if sm.smtpServer != nil { + logrus.Info("Closing SMTP server") + if err := sm.smtpServer.Close(); err != nil { + logrus.WithError(err).Debug("Failed to close SMTP server (expected -- we close the listener ourselves)") + } - bridge.publish(events.SMTPServerStopped{}) + sm.smtpServer = nil + + bridge.publish(events.SMTPServerStopped{}) + } return nil } func (sm *ServerManager) closeIMAPServer(ctx context.Context, bridge *Bridge) error { - logrus.Info("Closing IMAP server") + if sm.imapListener != nil { + logrus.Info("Closing IMAP Listener") + + if err := sm.imapListener.Close(); err != nil { + return fmt.Errorf("failed to close IMAP listener: %w", err) + } + + sm.imapListener = nil + + bridge.publish(events.IMAPServerStopped{}) + } if sm.imapServer != nil { + logrus.Info("Closing IMAP server") if err := sm.imapServer.Close(ctx); err != nil { return fmt.Errorf("failed to close IMAP server: %w", err) } @@ -386,16 +441,6 @@ func (sm *ServerManager) closeIMAPServer(ctx context.Context, bridge *Bridge) er sm.imapServer = nil } - if sm.imapListener != nil { - if err := sm.imapListener.Close(); err != nil { - return fmt.Errorf("failed to close IMAP listener: %w", err) - } - - sm.imapListener = nil - } - - bridge.publish(events.IMAPServerStopped{}) - return nil } @@ -412,7 +457,11 @@ func (sm *ServerManager) restartIMAP(ctx context.Context, bridge *Bridge) error bridge.publish(events.IMAPServerStopped{}) } - return sm.serveIMAP(ctx, bridge) + if sm.shouldStartServers() { + return sm.serveIMAP(ctx, bridge) + } + + return nil } func (sm *ServerManager) restartSMTP(bridge *Bridge) error { @@ -426,7 +475,11 @@ func (sm *ServerManager) restartSMTP(bridge *Bridge) error { sm.smtpServer = newSMTPServer(bridge, bridge.tlsConfig, bridge.logSMTP) - return sm.serveSMTP(bridge) + if sm.shouldStartServers() { + return sm.serveSMTP(bridge) + } + + return nil } func (sm *ServerManager) serveSMTP(bridge *Bridge) error { @@ -515,6 +568,21 @@ func (sm *ServerManager) serveIMAP(ctx context.Context, bridge *Bridge) error { return nil } +func (sm *ServerManager) stopIMAPListener(bridge *Bridge) error { + logrus.Info("Stopping IMAP listener") + if sm.imapListener != nil { + if err := sm.imapListener.Close(); err != nil { + return err + } + + sm.imapListener = nil + + bridge.publish(events.IMAPServerStopped{}) + } + + return nil +} + func (sm *ServerManager) handleSetGluonDir(ctx context.Context, bridge *Bridge, newGluonDir string) error { return safe.RLockRet(func() error { currentGluonDir := bridge.GetGluonCacheDir() @@ -527,6 +595,8 @@ func (sm *ServerManager) handleSetGluonDir(ctx context.Context, bridge *Bridge, return fmt.Errorf("failed to close IMAP: %w", err) } + sm.loadedUserCount = 0 + if err := bridge.moveGluonCacheDir(currentGluonDir, newGluonDir); err != nil { logrus.WithError(err).Error("failed to move GluonCacheDir") @@ -560,15 +630,17 @@ func (sm *ServerManager) handleSetGluonDir(ctx context.Context, bridge *Bridge, } sm.imapServer = imapServer - for _, bridgeUser := range bridge.users { if err := sm.handleAddIMAPUser(ctx, bridgeUser); err != nil { return fmt.Errorf("failed to add users to new IMAP server: %w", err) } + sm.loadedUserCount++ } - if err := sm.serveIMAP(ctx, bridge); err != nil { - return fmt.Errorf("failed to serve IMAP: %w", err) + if sm.shouldStartServers() { + if err := sm.serveIMAP(ctx, bridge); err != nil { + return fmt.Errorf("failed to serve IMAP: %w", err) + } } return nil @@ -591,6 +663,10 @@ func (sm *ServerManager) handleRemoveGluonUser(ctx context.Context, userID strin return sm.imapServer.RemoveUser(ctx, userID, true) } +func (sm *ServerManager) shouldStartServers() bool { + return sm.loadedUserCount >= 1 +} + type smRequestClose struct{} type smRequestRestartIMAP struct{} diff --git a/internal/bridge/server_manager_test.go b/internal/bridge/server_manager_test.go new file mode 100644 index 00000000..23a94a1d --- /dev/null +++ b/internal/bridge/server_manager_test.go @@ -0,0 +1,171 @@ +// Copyright (c) 2023 Proton AG +// +// This file is part of Proton Mail Bridge. +// +// Proton Mail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Proton Mail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Proton Mail Bridge. If not, see . + +package bridge_test + +import ( + "context" + "fmt" + "testing" + + "github.com/ProtonMail/go-proton-api" + "github.com/ProtonMail/go-proton-api/server" + "github.com/ProtonMail/proton-bridge/v3/internal/bridge" + "github.com/ProtonMail/proton-bridge/v3/internal/constants" + "github.com/ProtonMail/proton-bridge/v3/internal/events" + "github.com/emersion/go-imap/client" + "github.com/stretchr/testify/require" +) + +func TestServerManager_NoLoadedUsersNoServers(t *testing.T) { + withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) { + withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + _, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort())) + require.Error(t, err) + }) + }) +} + +func TestServerManager_ServersStartAfterFirstConnectedUser(t *testing.T) { + withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) { + withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + imapWaiter := waitForIMAPServerReady(bridge) + defer imapWaiter.Done() + + smtpWaiter := waitForSMTPServerReady(bridge) + defer smtpWaiter.Done() + + _, err := bridge.LoginFull(ctx, username, password, nil, nil) + require.NoError(t, err) + + imapWaiter.Wait() + smtpWaiter.Wait() + }) + }) +} + +func TestServerManager_ServersStopsAfterUserLogsOut(t *testing.T) { + withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) { + withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + imapWaiter := waitForIMAPServerReady(bridge) + defer imapWaiter.Done() + + smtpWaiter := waitForSMTPServerReady(bridge) + defer smtpWaiter.Done() + + userID, err := bridge.LoginFull(ctx, username, password, nil, nil) + require.NoError(t, err) + + imapWaiter.Wait() + smtpWaiter.Wait() + + imapWaiterStopped := waitForIMAPServerStopped(bridge) + defer imapWaiterStopped.Done() + + require.NoError(t, bridge.LogoutUser(ctx, userID)) + + imapWaiterStopped.Wait() + }) + }) +} + +func TestServerManager_ServersDoNotStopWhenThereIsStillOneActiveUser(t *testing.T) { + otherPassword := []byte("bar") + otherUser := "foo" + withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) { + _, _, err := s.CreateUser(otherUser, otherPassword) + require.NoError(t, err) + + withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + imapWaiter := waitForIMAPServerReady(bridge) + defer imapWaiter.Done() + + smtpWaiter := waitForSMTPServerReady(bridge) + defer smtpWaiter.Done() + + _, err := bridge.LoginFull(ctx, username, password, nil, nil) + require.NoError(t, err) + + userIDOther, err := bridge.LoginFull(ctx, otherUser, otherPassword, nil, nil) + require.NoError(t, err) + + imapWaiter.Wait() + smtpWaiter.Wait() + + evtCh, cancel := bridge.GetEvents(events.UserDeauth{}) + defer cancel() + + require.NoError(t, s.RevokeUser(userIDOther)) + + waitForEvent(t, evtCh, events.UserDeauth{}) + + imapClient, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort())) + require.NoError(t, err) + require.NoError(t, imapClient.Logout()) + }) + }) +} + +func TestServerManager_ServersStartIfAtLeastOneUserIsLoggedIn(t *testing.T) { + otherPassword := []byte("bar") + otherUser := "foo" + withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) { + userIDOther, _, err := s.CreateUser(otherUser, otherPassword) + require.NoError(t, err) + + withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + _, err := bridge.LoginFull(ctx, username, password, nil, nil) + require.NoError(t, err) + + _, err = bridge.LoginFull(ctx, otherUser, otherPassword, nil, nil) + require.NoError(t, err) + }) + + require.NoError(t, s.RevokeUser(userIDOther)) + + withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + imapClient, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort())) + require.NoError(t, err) + require.NoError(t, imapClient.Logout()) + }) + }) +} + +func TestServerManager_NetworkLossStopsServers(t *testing.T) { + withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) { + withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + imapWaiter := waitForIMAPServerReady(bridge) + defer imapWaiter.Done() + + imapWaiterStop := waitForIMAPServerStopped(bridge) + defer imapWaiterStop.Done() + + _, err := bridge.LoginFull(ctx, username, password, nil, nil) + require.NoError(t, err) + + imapWaiter.Wait() + + netCtl.Disable() + + imapWaiterStop.Wait() + + netCtl.Enable() + + imapWaiter.Wait() + }) + }) +} diff --git a/internal/bridge/sync_test.go b/internal/bridge/sync_test.go index a83dac42..c880f7b3 100644 --- a/internal/bridge/sync_test.go +++ b/internal/bridge/sync_test.go @@ -112,15 +112,6 @@ func TestBridge_Sync(t *testing.T) { info, err := b.GetUserInfo(userID) require.NoError(t, err) require.True(t, info.State == bridge.Connected) - - client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) - require.NoError(t, err) - require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) - defer func() { _ = client.Logout() }() - - status, err := client.Select(`Folders/folder`, false) - require.NoError(t, err) - require.Less(t, status.Messages, uint32(numMsg)) } // Remove the network limit, allowing the sync to finish. @@ -273,15 +264,6 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) { info, err := b.GetUserInfo(userID) require.NoError(t, err) require.True(t, info.State == bridge.Connected) - - client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) - require.NoError(t, err) - require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) - defer func() { _ = client.Logout() }() - - status, err := client.Select(`Folders/folder`, false) - require.NoError(t, err) - require.Less(t, status.Messages, uint32(numMsg)) } // Create a new mailbox and move that last 1/3 of the messages into it to simulate user diff --git a/internal/bridge/user_event_test.go b/internal/bridge/user_event_test.go index c5356a07..caae0134 100644 --- a/internal/bridge/user_event_test.go +++ b/internal/bridge/user_event_test.go @@ -141,6 +141,9 @@ func test_badMessage_badEvent(userFeedback func(t *testing.T, ctx context.Contex }) withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + smtpWaiter := waitForSMTPServerReady(bridge) + defer smtpWaiter.Done() + userLoginAndSync(ctx, t, bridge, "user", password) var messageIDs []string @@ -176,6 +179,8 @@ func test_badMessage_badEvent(userFeedback func(t *testing.T, ctx context.Contex userFeedback(t, ctx, bridge, badUserID) + smtpWaiter.Wait() + userContinueEventProcess(ctx, t, s, bridge) }) }) @@ -194,6 +199,9 @@ func TestBridge_User_BadMessage_NoBadEvent(t *testing.T) { }) withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + smtpWaiter := waitForSMTPServerReady(bridge) + defer smtpWaiter.Done() + userLoginAndSync(ctx, t, bridge, "user", password) var messageIDs []string @@ -217,6 +225,7 @@ func TestBridge_User_BadMessage_NoBadEvent(t *testing.T) { require.NoError(t, c.DeleteMessage(ctx, messageIDs...)) }) + smtpWaiter.Wait() userContinueEventProcess(ctx, t, s, bridge) }) }) @@ -412,6 +421,17 @@ func TestBridge_User_DropConn_NoBadEvent(t *testing.T) { }) withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + var count int32 + // The first 10 times bridge attempts to sync any of the messages, drop the connection. + s.AddStatusHook(func(req *http.Request) (int, bool) { + if strings.Contains(req.URL.Path, "/mail/v4/messages") { + if atomic.AddInt32(&count, 1) < 10 { + dropListener.DropAll() + } + } + + return 0, false + }) userLoginAndSync(ctx, t, bridge, "user", password) mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes() @@ -421,19 +441,6 @@ func TestBridge_User_DropConn_NoBadEvent(t *testing.T) { createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10) }) - var count int - - // The first 10 times bridge attempts to sync any of the messages, drop the connection. - s.AddStatusHook(func(req *http.Request) (int, bool) { - if strings.Contains(req.URL.Path, "/mail/v4/messages") { - if count++; count < 10 { - dropListener.DropAll() - } - } - - return 0, false - }) - info, err := bridge.QueryUserInfo("user") require.NoError(t, err) @@ -771,11 +778,16 @@ func TestBridge_User_CreateDisabledAddress(t *testing.T) { func TestBridge_User_HandleParentLabelRename(t *testing.T) { withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { + imapWaiter := waitForIMAPServerReady(bridge) + defer imapWaiter.Done() + require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil))) info, err := bridge.QueryUserInfo(username) require.NoError(t, err) + imapWaiter.Wait() + client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort())) require.NoError(t, err) require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) diff --git a/tests/ctx_test.go b/tests/ctx_test.go index 5fad8fee..b37a0a99 100644 --- a/tests/ctx_test.go +++ b/tests/ctx_test.go @@ -167,6 +167,8 @@ type testCtx struct { // This slice contains the dummy listeners that are intended to block network ports. dummyListeners []net.Listener + + imapServerStarted bool } type imapClient struct { diff --git a/tests/user_test.go b/tests/user_test.go index c06f0bd7..f1dffda8 100644 --- a/tests/user_test.go +++ b/tests/user_test.go @@ -28,6 +28,7 @@ import ( "github.com/ProtonMail/go-proton-api" "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/proton-bridge/v3/internal/bridge" + "github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/pkg/algo" "github.com/bradenaw/juniper/iterator" @@ -331,10 +332,20 @@ func (s *scenario) drafAtIndexWasMovedToTrashForAddressOfAccount(draftIndex int, } func (s *scenario) userLogsInWithUsernameAndPassword(username, password string) error { + evtCh, cancel := s.t.bridge.GetEvents(events.SMTPServerReady{}) + defer cancel() + userID, err := s.t.bridge.LoginFull(context.Background(), username, []byte(password), nil, nil) if err != nil { s.t.pushError(err) } else { + // We need to wait for server to be up or we won't be able to connect. It should only happen once to avoid + // blocking on multiple Logins. + if !s.t.imapServerStarted { + <-evtCh + s.t.imapServerStarted = true + } + if userID != s.t.getUserByName(username).getUserID() { return errors.New("user ID mismatch") } From a3e07428b59a47b2a52eee76ddac647834cb7d49 Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Thu, 11 May 2023 16:18:43 +0200 Subject: [PATCH 23/43] chore: Improve CPC code * Remove distinction between values with and without reply. * Hide types that don't need to be public. * Don't allow direct access to the request's internal types. --- cpc/cpc.go | 159 ------------------------------ internal/bridge/server_manager.go | 36 +++---- pkg/cpc/cpc.go | 129 ++++++++++++++++++++++++ {cpc => pkg/cpc}/cpc_test.go | 12 ++- 4 files changed, 154 insertions(+), 182 deletions(-) delete mode 100644 cpc/cpc.go create mode 100644 pkg/cpc/cpc.go rename {cpc => pkg/cpc}/cpc_test.go (83%) diff --git a/cpc/cpc.go b/cpc/cpc.go deleted file mode 100644 index dbe9bc29..00000000 --- a/cpc/cpc.go +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) 2023 Proton AG -// -// This file is part of Proton Mail Bridge. -// -// Proton Mail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Proton Mail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Proton Mail Bridge. If not, see . - -package cpc - -import ( - "context" - "errors" - "fmt" -) - -var ErrRequestHasNoReply = errors.New("request has no reply channel") -var ErrExpectedReply = errors.New("request does not have reply channel") - -// Utilities to implement Chanel Procedure Calls. Similar in concept to RPC, but with between go-routines. - -type RequestReply struct { - Value any - Error error -} - -type Request struct { - Value any - Reply chan RequestReply -} - -func NewRequest(value any) *Request { - return &Request{ - Value: value, - Reply: make(chan RequestReply), - } -} - -func NewRequestWithoutReply(value any) *Request { - return &Request{ - Value: value, - Reply: nil, - } -} - -func (r *Request) Close() { - if r.Reply != nil { - panic("request reply has not been sent") - } -} - -func (r *Request) SendReply(ctx context.Context, value any, err error) { - if r.Reply == nil { - panic("request has no reply") - } - - defer func() { - close(r.Reply) - r.Reply = nil - }() - - select { - case <-ctx.Done(): - case r.Reply <- RequestReply{ - Value: value, - Error: err, - }: - } -} - -type CPC struct { - request chan *Request -} - -func NewCPC() *CPC { - return &CPC{ - request: make(chan *Request), - } -} - -// Receive is meant to be called by the code that is supposed to handle the requests that arrive. -func (c *CPC) Receive(ctx context.Context, f func(context.Context, *Request)) { - for request := range c.request { - f(ctx, request) - request.Close() - } -} - -func (c *CPC) ReceiveCh() <-chan *Request { - return c.request -} - -func (c *CPC) Close() { - close(c.request) -} - -// SendNoReply sends a request which doesn't expect a reply. -func (c *CPC) SendNoReply(ctx context.Context, value any) error { - return c.executeNoReplyImpl(ctx, NewRequestWithoutReply(value)) -} - -// SendWithReply sends a request which expects a reply. -func (c *CPC) SendWithReply(ctx context.Context, value any) (any, error) { - return c.executeReplyImpl(ctx, NewRequest(value)) -} - -func SendWithReplyType[T any](ctx context.Context, c *CPC, value any) (T, error) { - val, err := c.executeReplyImpl(ctx, NewRequest(value)) - if err != nil { - var t T - return t, err - } - - switch vt := val.(type) { - case T: - return vt, nil - default: - var t T - return t, fmt.Errorf("reply type does not match") - } -} - -func (c *CPC) executeNoReplyImpl(ctx context.Context, request *Request) error { - select { - case <-ctx.Done(): - return ctx.Err() - case c.request <- request: - } - - return nil -} - -func (c *CPC) executeReplyImpl(ctx context.Context, request *Request) (any, error) { - if request.Reply == nil { - return nil, ErrExpectedReply - } - - select { - case <-ctx.Done(): - return nil, ctx.Err() - case c.request <- request: - } - - select { - case <-ctx.Done(): - return nil, ctx.Err() - case reply := <-request.Reply: - return reply.Value, reply.Error - } -} diff --git a/internal/bridge/server_manager.go b/internal/bridge/server_manager.go index acca55c8..9e06a430 100644 --- a/internal/bridge/server_manager.go +++ b/internal/bridge/server_manager.go @@ -26,10 +26,10 @@ import ( "github.com/ProtonMail/gluon" "github.com/ProtonMail/gluon/connector" "github.com/ProtonMail/gluon/logging" - "github.com/ProtonMail/proton-bridge/v3/cpc" "github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/safe" "github.com/ProtonMail/proton-bridge/v3/internal/user" + "github.com/ProtonMail/proton-bridge/v3/pkg/cpc" "github.com/emersion/go-smtp" "github.com/sirupsen/logrus" ) @@ -77,31 +77,31 @@ func (sm *ServerManager) Init(bridge *Bridge) error { func (sm *ServerManager) CloseServers(ctx context.Context) error { defer sm.requests.Close() - _, err := sm.requests.SendWithReply(ctx, &smRequestClose{}) + _, err := sm.requests.Send(ctx, &smRequestClose{}) return err } func (sm *ServerManager) RestartIMAP(ctx context.Context) error { - _, err := sm.requests.SendWithReply(ctx, &smRequestRestartIMAP{}) + _, err := sm.requests.Send(ctx, &smRequestRestartIMAP{}) return err } func (sm *ServerManager) RestartSMTP(ctx context.Context) error { - _, err := sm.requests.SendWithReply(ctx, &smRequestRestartSMTP{}) + _, err := sm.requests.Send(ctx, &smRequestRestartSMTP{}) return err } func (sm *ServerManager) AddIMAPUser(ctx context.Context, user *user.User) error { - _, err := sm.requests.SendWithReply(ctx, &smRequestAddIMAPUser{user: user}) + _, err := sm.requests.Send(ctx, &smRequestAddIMAPUser{user: user}) return err } func (sm *ServerManager) RemoveIMAPUser(ctx context.Context, user *user.User, withData bool) error { - _, err := sm.requests.SendWithReply(ctx, &smRequestRemoveIMAPUser{ + _, err := sm.requests.Send(ctx, &smRequestRemoveIMAPUser{ user: user, withData: withData, }) @@ -110,7 +110,7 @@ func (sm *ServerManager) RemoveIMAPUser(ctx context.Context, user *user.User, wi } func (sm *ServerManager) SetGluonDir(ctx context.Context, gluonDir string) error { - _, err := sm.requests.SendWithReply(ctx, &smRequestSetGluonDir{ + _, err := sm.requests.Send(ctx, &smRequestSetGluonDir{ dir: gluonDir, }) @@ -118,7 +118,7 @@ func (sm *ServerManager) SetGluonDir(ctx context.Context, gluonDir string) error } func (sm *ServerManager) AddGluonUser(ctx context.Context, conn connector.Connector, passphrase []byte) (string, error) { - reply, err := cpc.SendWithReplyType[string](ctx, sm.requests, &smRequestAddGluonUser{ + reply, err := cpc.SendTyped[string](ctx, sm.requests, &smRequestAddGluonUser{ conn: conn, passphrase: passphrase, }) @@ -127,7 +127,7 @@ func (sm *ServerManager) AddGluonUser(ctx context.Context, conn connector.Connec } func (sm *ServerManager) RemoveGluonUser(ctx context.Context, gluonID string) error { - _, err := sm.requests.SendWithReply(ctx, &smRequestRemoveGluonUser{ + _, err := sm.requests.Send(ctx, &smRequestRemoveGluonUser{ userID: gluonID, }) @@ -165,23 +165,23 @@ func (sm *ServerManager) run(ctx context.Context, bridge *Bridge) { return } - switch r := request.Value.(type) { + switch r := request.Value().(type) { case *smRequestClose: sm.handleClose(ctx, bridge) - request.SendReply(ctx, nil, nil) + request.Reply(ctx, nil, nil) return case *smRequestRestartSMTP: err := sm.restartSMTP(bridge) - request.SendReply(ctx, nil, err) + request.Reply(ctx, nil, err) case *smRequestRestartIMAP: err := sm.restartIMAP(ctx, bridge) - request.SendReply(ctx, nil, err) + request.Reply(ctx, nil, err) case *smRequestAddIMAPUser: err := sm.handleAddIMAPUser(ctx, r.user) - request.SendReply(ctx, nil, err) + request.Reply(ctx, nil, err) if err == nil { sm.loadedUserCount++ sm.handleLoadedUserCountChange(ctx, bridge) @@ -189,7 +189,7 @@ func (sm *ServerManager) run(ctx context.Context, bridge *Bridge) { case *smRequestRemoveIMAPUser: err := sm.handleRemoveIMAPUser(ctx, r.user, r.withData) - request.SendReply(ctx, nil, err) + request.Reply(ctx, nil, err) if err == nil { sm.loadedUserCount-- sm.handleLoadedUserCountChange(ctx, bridge) @@ -197,15 +197,15 @@ func (sm *ServerManager) run(ctx context.Context, bridge *Bridge) { case *smRequestSetGluonDir: err := sm.handleSetGluonDir(ctx, bridge, r.dir) - request.SendReply(ctx, nil, err) + request.Reply(ctx, nil, err) case *smRequestAddGluonUser: id, err := sm.handleAddGluonUser(ctx, r.conn, r.passphrase) - request.SendReply(ctx, id, err) + request.Reply(ctx, id, err) case *smRequestRemoveGluonUser: err := sm.handleRemoveGluonUser(ctx, r.userID) - request.SendReply(ctx, nil, err) + request.Reply(ctx, nil, err) } } } diff --git a/pkg/cpc/cpc.go b/pkg/cpc/cpc.go new file mode 100644 index 00000000..0892c28b --- /dev/null +++ b/pkg/cpc/cpc.go @@ -0,0 +1,129 @@ +// Copyright (c) 2023 Proton AG +// +// This file is part of Proton Mail Bridge. +// +// Proton Mail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Proton Mail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Proton Mail Bridge. If not, see . + +package cpc + +import ( + "context" + "errors" +) + +var ErrInvalidReplyType = errors.New("reply type does not match") + +// Utilities to implement Chanel Procedure Calls. Similar in concept to RPC, but with between go-routines. + +// Request contains the data for a request as well as the means to reply to a request. +type Request struct { + value any + reply chan reply +} + +// Value returns the request value. +func (r *Request) Value() any { + return r.value +} + +// Reply should be used to send a reply to a given request. +func (r *Request) Reply(ctx context.Context, value any, err error) { + defer close(r.reply) + + select { + case <-ctx.Done(): + case r.reply <- reply{ + value: value, + error: err, + }: + } +} + +// CPC Channel Procedure Call. A play on RPC, but with channels. Use this type to send requests and wait for replies +// from a goroutine. +type CPC struct { + request chan *Request +} + +func NewCPC() *CPC { + return &CPC{ + request: make(chan *Request), + } +} + +// Receive invokes the function on all the request that arrive. +func (c *CPC) Receive(ctx context.Context, f func(context.Context, *Request)) { + for request := range c.request { + f(ctx, request) + } +} + +// ReceiveCh returns the channel on which all requests are sent. +func (c *CPC) ReceiveCh() <-chan *Request { + return c.request +} + +// Close closes the CPC channel and no further requests should be made. +func (c *CPC) Close() { + close(c.request) +} + +// Send sends a request which expects a reply. +func (c *CPC) Send(ctx context.Context, value any) (any, error) { + return c.execute(ctx, newRequest(value)) +} + +// SendTyped is similar to CPC.Send, but ensure that reply is of the given Type T. +func SendTyped[T any](ctx context.Context, c *CPC, value any) (T, error) { + val, err := c.execute(ctx, newRequest(value)) + if err != nil { + var t T + return t, err + } + + switch vt := val.(type) { + case T: + return vt, nil + default: + var t T + return t, ErrInvalidReplyType + } +} + +type reply struct { + value any + error error +} + +func (c *CPC) execute(ctx context.Context, request *Request) (any, error) { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case c.request <- request: + } + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case r := <-request.reply: + return r.value, r.error + } +} + +func newRequest(value any) *Request { + return &Request{ + value: value, + reply: make(chan reply), + } +} diff --git a/cpc/cpc_test.go b/pkg/cpc/cpc_test.go similarity index 83% rename from cpc/cpc_test.go rename to pkg/cpc/cpc_test.go index 8fc70406..102970e5 100644 --- a/cpc/cpc_test.go +++ b/pkg/cpc/cpc_test.go @@ -42,22 +42,24 @@ func TestCPC_Receive(t *testing.T) { wg.Add(1) cpc.Receive(context.Background(), func(ctx context.Context, request *Request) { - switch request.Value.(type) { + switch request.Value().(type) { case sendIntRequest: - request.SendReply(ctx, replyValue, nil) + request.Reply(ctx, replyValue, nil) case quitRequest: - cpc.Close() + request.Reply(ctx, nil, nil) default: panic("unknown request") } }) }() - r, err := cpc.SendWithReply(context.Background(), sendIntRequest{}) + r, err := cpc.Send(context.Background(), sendIntRequest{}) require.NoError(t, err) require.Equal(t, r, replyValue) - require.NoError(t, cpc.SendNoReply(context.Background(), quitRequest{})) + _, err = cpc.Send(context.Background(), quitRequest{}) + require.NoError(t, err) + cpc.Close() wg.Wait() } From 6adb440b841f1c7d4e37f01bcece09409d0e6bba Mon Sep 17 00:00:00 2001 From: Romain LE JEUNE Date: Thu, 11 May 2023 13:34:13 +0200 Subject: [PATCH 24/43] fix(GODT-2623): log IMAP/SMTP login failure as error. --- internal/bridge/imap.go | 3 ++- internal/bridge/smtp_backend.go | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/bridge/imap.go b/internal/bridge/imap.go index f5f30275..cf43c619 100644 --- a/internal/bridge/imap.go +++ b/internal/bridge/imap.go @@ -79,7 +79,8 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) { logrus.WithFields(logrus.Fields{ "sessionID": event.SessionID, "username": event.Username, - }).Info("Received IMAP login failure notification") + "pkg": "imap", + }).Error("Incorrect login credentials.") bridge.publish(events.IMAPLoginFailed{Username: event.Username}) } } diff --git a/internal/bridge/smtp_backend.go b/internal/bridge/smtp_backend.go index 014edd81..996bf5be 100644 --- a/internal/bridge/smtp_backend.go +++ b/internal/bridge/smtp_backend.go @@ -58,6 +58,11 @@ func (s *smtpSession) AuthPlain(username, password string) error { return nil } + logrus.WithFields(logrus.Fields{ + "username": username, + "pkg": "smtp", + }).Error("Incorrect login credentials.") + return fmt.Errorf("invalid username or password") }, s.usersLock) } From d08b3fcca4dee8b1741a6147b3586cdd7a145979 Mon Sep 17 00:00:00 2001 From: Romain LE JEUNE Date: Thu, 11 May 2023 17:09:51 +0200 Subject: [PATCH 25/43] chore: extend the timeout for integration test form 20m to 30. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 39ffc991..500e71e5 100644 --- a/Makefile +++ b/Makefile @@ -234,7 +234,7 @@ test-race: gofiles go test -v -timeout=30m -p=1 -count=1 -race -failfast -run=${TESTRUN} ./internal/... ./pkg/... test-integration: gofiles - go test -v -timeout=20m -p=1 -count=1 github.com/ProtonMail/proton-bridge/v3/tests + go test -v -timeout=30m -p=1 -count=1 github.com/ProtonMail/proton-bridge/v3/tests test-integration-debug: gofiles dlv test github.com/ProtonMail/proton-bridge/v3/tests -- -test.v -test.timeout=10m -test.parallel=1 -test.count=1 From 98031d296ea95fe38a4b09afb3690661359c72cc Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Fri, 12 May 2023 10:19:46 +0200 Subject: [PATCH 26/43] Revert "fix(GODT-2588): Always perma-delete from Drafts/Trash" This reverts commit f9a0c35daa7217d4bebd8d8d93a8720336eeec04. --- internal/user/imap.go | 28 +++++++++++++++++-- .../imap/message/delete_from_trash.feature | 6 ++-- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/internal/user/imap.go b/internal/user/imap.go index 97841a4d..b6257cba 100644 --- a/internal/user/imap.go +++ b/internal/user/imap.go @@ -407,8 +407,32 @@ func (conn *imapConnector) RemoveMessagesFromMailbox(ctx context.Context, messag } if mailboxID == proton.TrashLabel || mailboxID == proton.DraftsLabel { - if err := conn.client.DeleteMessage(ctx, xslices.Map(messageIDs, func(m imap.MessageID) string { - return string(m) + var metadata []proton.MessageMetadata + + // There's currently no limit on how many IDs we can filter on, + // but to be nice to API, let's chunk it by 150. + for _, messageIDs := range xslices.Chunk(messageIDs, 150) { + m, err := conn.client.GetMessageMetadata(ctx, proton.MessageFilter{ + ID: mapTo[imap.MessageID, string](messageIDs), + }) + if err != nil { + return err + } + + // If a message is not preset in any other label other than AllMail, AllDrafts and AllSent, it can be + // permanently deleted. + m = xslices.Filter(m, func(m proton.MessageMetadata) bool { + labelsThatMatter := xslices.Filter(m.LabelIDs, func(id string) bool { + return id != proton.AllDraftsLabel && id != proton.AllMailLabel && id != proton.AllSentLabel + }) + return len(labelsThatMatter) == 0 + }) + + metadata = append(metadata, m...) + } + + if err := conn.client.DeleteMessage(ctx, xslices.Map(metadata, func(m proton.MessageMetadata) string { + return m.ID })...); err != nil { return err } diff --git a/tests/features/imap/message/delete_from_trash.feature b/tests/features/imap/message/delete_from_trash.feature index 7d80ece4..fdc5105e 100644 --- a/tests/features/imap/message/delete_from_trash.feature +++ b/tests/features/imap/message/delete_from_trash.feature @@ -6,7 +6,7 @@ Feature: IMAP remove messages from Trash | mbox | folder | | label | label | - Scenario Outline: Message in Trash and some other label is permanently deleted + Scenario Outline: Message in Trash and some other label is not permanently deleted Given the address "[user:user]@[domain]" of account "[user:user]" has the following messages in "Trash": | from | to | subject | body | | john.doe@mail.com | [user:user]@[domain] | foo | hello | @@ -26,8 +26,8 @@ Feature: IMAP remove messages from Trash When IMAP client "1" expunges Then it succeeds And IMAP client "1" eventually sees 1 messages in "Trash" - And IMAP client "1" eventually sees 1 messages in "All Mail" - And IMAP client "1" eventually sees 0 messages in "Labels/label" + And IMAP client "1" eventually sees 2 messages in "All Mail" + And IMAP client "1" eventually sees 1 messages in "Labels/label" Scenario Outline: Message in Trash only is permanently deleted Given the address "[user:user]@[domain]" of account "[user:user]" has the following messages in "Trash": From faf3780eee6d0c24134adab228c845c91c1c236d Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Fri, 12 May 2023 15:37:33 +0200 Subject: [PATCH 27/43] test: Fix TestBridge_Report --- internal/bridge/sentry_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/bridge/sentry_test.go b/internal/bridge/sentry_test.go index afff01bc..291493f8 100644 --- a/internal/bridge/sentry_test.go +++ b/internal/bridge/sentry_test.go @@ -36,6 +36,9 @@ import ( func TestBridge_Report(t *testing.T) { withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) { + imapWaiter := waitForIMAPServerReady(b) + defer imapWaiter.Done() + syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{})) defer done() @@ -51,6 +54,8 @@ func TestBridge_Report(t *testing.T) { require.NoError(t, err) require.True(t, info.State == bridge.Connected) + imapWaiter.Wait() + // Dial the IMAP port. conn, err := net.Dial("tcp", fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) require.NoError(t, err) From 0ef9c9c9c9f999852bec47da9046f211eba72c26 Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Fri, 12 May 2023 16:01:31 +0200 Subject: [PATCH 28/43] fix(GODT-2606): Improve Vault concurrency scopes Rewrite the vault to have one RWlock rather then two separate locks for data and reference counts. In certain circumstance, it could be possible that that different requests could end up in undefined states if a user got deleted successfully while at he same time another goroutine/thread is loading the given user. While I have not been able to reproduce this in a test, restricting the access scope to one lock rather than two, should avoid corner cases where logic code is executing outside of the lock scope. --- internal/bridge/user.go | 24 +---- internal/vault/certs.go | 17 +-- internal/vault/cookies.go | 4 +- internal/vault/settings.go | 72 ++++++------- internal/vault/user.go | 8 ++ internal/vault/vault.go | 195 +++++++++++++++++++++++++++------- internal/vault/vault_debug.go | 4 +- 7 files changed, 214 insertions(+), 110 deletions(-) diff --git a/internal/bridge/user.go b/internal/bridge/user.go index 526850b1..3c8e15e4 100644 --- a/internal/bridge/user.go +++ b/internal/bridge/user.go @@ -584,29 +584,7 @@ func (bridge *Bridge) newVaultUser( authUID, authRef string, saltedKeyPass []byte, ) (*vault.User, bool, error) { - if !bridge.vault.HasUser(apiUser.ID) { - user, err := bridge.vault.AddUser(apiUser.ID, apiUser.Name, apiUser.Email, authUID, authRef, saltedKeyPass) - if err != nil { - return nil, false, fmt.Errorf("failed to add user to vault: %w", err) - } - - return user, true, nil - } - - user, err := bridge.vault.NewUser(apiUser.ID) - if err != nil { - return nil, false, err - } - - if err := user.SetAuth(authUID, authRef); err != nil { - return nil, false, err - } - - if err := user.SetKeyPass(saltedKeyPass); err != nil { - return nil, false, err - } - - return user, false, nil + return bridge.vault.GetOrAddUser(apiUser.ID, apiUser.Name, apiUser.Email, authUID, authRef, saltedKeyPass) } // logout logs out the given user, optionally logging them out from the API too. diff --git a/internal/vault/certs.go b/internal/vault/certs.go index 24be5f38..ee48e203 100644 --- a/internal/vault/certs.go +++ b/internal/vault/certs.go @@ -30,7 +30,12 @@ import ( // If CertPEMPath is set, it will attempt to read the certificate from the file. // Otherwise, or on read/validation failure, it will return the certificate from the vault. func (vault *Vault) GetBridgeTLSCert() ([]byte, []byte) { - if certPath, keyPath := vault.get().Certs.CustomCertPath, vault.get().Certs.CustomKeyPath; certPath != "" && keyPath != "" { + vault.lock.RLock() + defer vault.lock.RUnlock() + + certs := vault.getUnsafe().Certs + + if certPath, keyPath := certs.CustomCertPath, certs.CustomKeyPath; certPath != "" && keyPath != "" { if certPEM, keyPEM, err := readPEMCert(certPath, keyPath); err == nil { return certPEM, keyPEM } @@ -38,7 +43,7 @@ func (vault *Vault) GetBridgeTLSCert() ([]byte, []byte) { logrus.Error("Failed to read certificate from file, using default") } - return vault.get().Certs.Bridge.Cert, vault.get().Certs.Bridge.Key + return certs.Bridge.Cert, certs.Bridge.Key } // SetBridgeTLSCertPath sets the path to PEM-encoded certificates for the bridge. @@ -47,7 +52,7 @@ func (vault *Vault) SetBridgeTLSCertPath(certPath, keyPath string) error { return fmt.Errorf("invalid certificate: %w", err) } - return vault.mod(func(data *Data) { + return vault.modSafe(func(data *Data) { data.Certs.CustomCertPath = certPath data.Certs.CustomKeyPath = keyPath }) @@ -55,18 +60,18 @@ func (vault *Vault) SetBridgeTLSCertPath(certPath, keyPath string) error { // SetBridgeTLSCertKey sets the path to PEM-encoded certificates for the bridge. func (vault *Vault) SetBridgeTLSCertKey(cert, key []byte) error { - return vault.mod(func(data *Data) { + return vault.modSafe(func(data *Data) { data.Certs.Bridge.Cert = cert data.Certs.Bridge.Key = key }) } func (vault *Vault) GetCertsInstalled() bool { - return vault.get().Certs.Installed + return vault.getSafe().Certs.Installed } func (vault *Vault) SetCertsInstalled(installed bool) error { - return vault.mod(func(data *Data) { + return vault.modSafe(func(data *Data) { data.Certs.Installed = installed }) } diff --git a/internal/vault/cookies.go b/internal/vault/cookies.go index afbe6a57..96ccda66 100644 --- a/internal/vault/cookies.go +++ b/internal/vault/cookies.go @@ -18,11 +18,11 @@ package vault func (vault *Vault) GetCookies() ([]byte, error) { - return vault.get().Cookies, nil + return vault.getSafe().Cookies, nil } func (vault *Vault) SetCookies(cookies []byte) error { - return vault.mod(func(data *Data) { + return vault.modSafe(func(data *Data) { data.Cookies = cookies }) } diff --git a/internal/vault/settings.go b/internal/vault/settings.go index f625825e..9a7815b3 100644 --- a/internal/vault/settings.go +++ b/internal/vault/settings.go @@ -33,72 +33,72 @@ const ( // GetIMAPPort sets the port that the IMAP server should listen on. func (vault *Vault) GetIMAPPort() int { - return vault.get().Settings.IMAPPort + return vault.getSafe().Settings.IMAPPort } // SetIMAPPort sets the port that the IMAP server should listen on. func (vault *Vault) SetIMAPPort(port int) error { - return vault.mod(func(data *Data) { + return vault.modSafe(func(data *Data) { data.Settings.IMAPPort = port }) } // GetSMTPPort sets the port that the SMTP server should listen on. func (vault *Vault) GetSMTPPort() int { - return vault.get().Settings.SMTPPort + return vault.getSafe().Settings.SMTPPort } // SetSMTPPort sets the port that the SMTP server should listen on. func (vault *Vault) SetSMTPPort(port int) error { - return vault.mod(func(data *Data) { + return vault.modSafe(func(data *Data) { data.Settings.SMTPPort = port }) } // GetIMAPSSL sets whether the IMAP server should use SSL. func (vault *Vault) GetIMAPSSL() bool { - return vault.get().Settings.IMAPSSL + return vault.getSafe().Settings.IMAPSSL } // SetIMAPSSL sets whether the IMAP server should use SSL. func (vault *Vault) SetIMAPSSL(ssl bool) error { - return vault.mod(func(data *Data) { + return vault.modSafe(func(data *Data) { data.Settings.IMAPSSL = ssl }) } // GetSMTPSSL sets whether the SMTP server should use SSL. func (vault *Vault) GetSMTPSSL() bool { - return vault.get().Settings.SMTPSSL + return vault.getSafe().Settings.SMTPSSL } // SetSMTPSSL sets whether the SMTP server should use SSL. func (vault *Vault) SetSMTPSSL(ssl bool) error { - return vault.mod(func(data *Data) { + return vault.modSafe(func(data *Data) { data.Settings.SMTPSSL = ssl }) } // GetGluonCacheDir sets the directory where the gluon should store its data. func (vault *Vault) GetGluonCacheDir() string { - return vault.get().Settings.GluonDir + return vault.getSafe().Settings.GluonDir } // SetGluonDir sets the directory where the gluon should store its data. func (vault *Vault) SetGluonDir(dir string) error { - return vault.mod(func(data *Data) { + return vault.modSafe(func(data *Data) { data.Settings.GluonDir = dir }) } // GetUpdateChannel sets the update channel. func (vault *Vault) GetUpdateChannel() updater.Channel { - return vault.get().Settings.UpdateChannel + return vault.getSafe().Settings.UpdateChannel } // SetUpdateChannel sets the update channel. func (vault *Vault) SetUpdateChannel(channel updater.Channel) error { - return vault.mod(func(data *Data) { + return vault.modSafe(func(data *Data) { data.Settings.UpdateChannel = channel }) } @@ -106,7 +106,7 @@ func (vault *Vault) SetUpdateChannel(channel updater.Channel) error { // GetUpdateRollout sets the update rollout. func (vault *Vault) GetUpdateRollout() float64 { // The rollout value 0.6046602879796196 is forbidden. The RNG was not seeded when it was picked (GODT-2319). - rollout := vault.get().Settings.UpdateRollout + rollout := vault.getSafe().Settings.UpdateRollout if math.Abs(rollout-ForbiddenRollout) >= 0.00000001 { return rollout } @@ -120,110 +120,110 @@ func (vault *Vault) GetUpdateRollout() float64 { // SetUpdateRollout sets the update rollout. func (vault *Vault) SetUpdateRollout(rollout float64) error { - return vault.mod(func(data *Data) { + return vault.modSafe(func(data *Data) { data.Settings.UpdateRollout = rollout }) } // GetColorScheme sets the color scheme to be used by the bridge GUI. func (vault *Vault) GetColorScheme() string { - return vault.get().Settings.ColorScheme + return vault.getSafe().Settings.ColorScheme } // SetColorScheme sets the color scheme to be used by the bridge GUI. func (vault *Vault) SetColorScheme(colorScheme string) error { - return vault.mod(func(data *Data) { + return vault.modSafe(func(data *Data) { data.Settings.ColorScheme = colorScheme }) } // GetProxyAllowed sets whether the bridge is allowed to use alternative routing. func (vault *Vault) GetProxyAllowed() bool { - return vault.get().Settings.ProxyAllowed + return vault.getSafe().Settings.ProxyAllowed } // SetProxyAllowed sets whether the bridge is allowed to use alternative routing. func (vault *Vault) SetProxyAllowed(allowed bool) error { - return vault.mod(func(data *Data) { + return vault.modSafe(func(data *Data) { data.Settings.ProxyAllowed = allowed }) } // GetShowAllMail sets whether the bridge should show the All Mail folder. func (vault *Vault) GetShowAllMail() bool { - return vault.get().Settings.ShowAllMail + return vault.getSafe().Settings.ShowAllMail } // SetShowAllMail sets whether the bridge should show the All Mail folder. func (vault *Vault) SetShowAllMail(showAllMail bool) error { - return vault.mod(func(data *Data) { + return vault.modSafe(func(data *Data) { data.Settings.ShowAllMail = showAllMail }) } // GetAutostart sets whether the bridge should autostart. func (vault *Vault) GetAutostart() bool { - return vault.get().Settings.Autostart + return vault.getSafe().Settings.Autostart } // SetAutostart sets whether the bridge should autostart. func (vault *Vault) SetAutostart(autostart bool) error { - return vault.mod(func(data *Data) { + return vault.modSafe(func(data *Data) { data.Settings.Autostart = autostart }) } // GetAutoUpdate sets whether the bridge should automatically update. func (vault *Vault) GetAutoUpdate() bool { - return vault.get().Settings.AutoUpdate + return vault.getSafe().Settings.AutoUpdate } // SetAutoUpdate sets whether the bridge should automatically update. func (vault *Vault) SetAutoUpdate(autoUpdate bool) error { - return vault.mod(func(data *Data) { + return vault.modSafe(func(data *Data) { data.Settings.AutoUpdate = autoUpdate }) } // GetTelemetryDisabled checks whether telemetry is disabled. func (vault *Vault) GetTelemetryDisabled() bool { - return vault.get().Settings.TelemetryDisabled + return vault.getSafe().Settings.TelemetryDisabled } // SetTelemetryDisabled sets whether telemetry is disabled. func (vault *Vault) SetTelemetryDisabled(telemetryDisabled bool) error { - return vault.mod(func(data *Data) { + return vault.modSafe(func(data *Data) { data.Settings.TelemetryDisabled = telemetryDisabled }) } // GetLastVersion returns the last version of the bridge that was run. func (vault *Vault) GetLastVersion() *semver.Version { - return semver.MustParse(vault.get().Settings.LastVersion) + return semver.MustParse(vault.getSafe().Settings.LastVersion) } // SetLastVersion sets the last version of the bridge that was run. func (vault *Vault) SetLastVersion(version *semver.Version) error { - return vault.mod(func(data *Data) { + return vault.modSafe(func(data *Data) { data.Settings.LastVersion = version.String() }) } // GetFirstStart returns whether this is the first time the bridge has been started. func (vault *Vault) GetFirstStart() bool { - return vault.get().Settings.FirstStart + return vault.getSafe().Settings.FirstStart } // SetFirstStart sets whether this is the first time the bridge has been started. func (vault *Vault) SetFirstStart(firstStart bool) error { - return vault.mod(func(data *Data) { + return vault.modSafe(func(data *Data) { data.Settings.FirstStart = firstStart }) } // GetMaxSyncMemory returns the maximum amount of memory the sync process should use. func (vault *Vault) GetMaxSyncMemory() uint64 { - v := vault.get().Settings.MaxSyncMemory + v := vault.getSafe().Settings.MaxSyncMemory // can be zero if never written to vault before. if v == 0 { return DefaultMaxSyncMemory @@ -234,14 +234,14 @@ func (vault *Vault) GetMaxSyncMemory() uint64 { // SetMaxSyncMemory sets the maximum amount of memory the sync process should use. func (vault *Vault) SetMaxSyncMemory(maxMemory uint64) error { - return vault.mod(func(data *Data) { + return vault.modSafe(func(data *Data) { data.Settings.MaxSyncMemory = maxMemory }) } // GetLastUserAgent returns the last user agent recorded by bridge. func (vault *Vault) GetLastUserAgent() string { - v := vault.get().Settings.LastUserAgent + v := vault.getSafe().Settings.LastUserAgent // Handle case where there may be no value. if len(v) == 0 { @@ -253,19 +253,19 @@ func (vault *Vault) GetLastUserAgent() string { // SetLastUserAgent store the last user agent recorded by bridge. func (vault *Vault) SetLastUserAgent(userAgent string) error { - return vault.mod(func(data *Data) { + return vault.modSafe(func(data *Data) { data.Settings.LastUserAgent = userAgent }) } // GetLastHeartbeatSent returns the last time heartbeat was sent. func (vault *Vault) GetLastHeartbeatSent() time.Time { - return vault.get().Settings.LastHeartbeatSent + return vault.getSafe().Settings.LastHeartbeatSent } // SetLastHeartbeatSent store the last time heartbeat was sent. func (vault *Vault) SetLastHeartbeatSent(timestamp time.Time) error { - return vault.mod(func(data *Data) { + return vault.modSafe(func(data *Data) { data.Settings.LastHeartbeatSent = timestamp }) } diff --git a/internal/vault/user.go b/internal/vault/user.go index f97358dc..1f997984 100644 --- a/internal/vault/user.go +++ b/internal/vault/user.go @@ -122,6 +122,14 @@ func (user *User) SetAuth(authUID, authRef string) error { }) } +func (user *User) setAuthAndKeyPassUnsafe(authUID, authRef string, keyPass []byte) error { + return user.vault.modUserUnsafe(user.userID, func(userData *UserData) { + userData.AuthRef = authRef + userData.AuthUID = authUID + userData.KeyPass = keyPass + }) +} + // KeyPass returns the user's (salted) key password. func (user *User) KeyPass() []byte { return user.vault.getUser(user.userID).KeyPass diff --git a/internal/vault/vault.go b/internal/vault/vault.go index e57c623f..e8cbc3b1 100644 --- a/internal/vault/vault.go +++ b/internal/vault/vault.go @@ -40,11 +40,11 @@ type Vault struct { path string gcm cipher.AEAD - enc []byte - encLock sync.RWMutex + enc []byte - ref map[string]int - refLock sync.Mutex + ref map[string]int + + lock sync.RWMutex panicHandler async.PanicHandler } @@ -79,14 +79,46 @@ func New(vaultDir, gluonCacheDir string, key []byte, panicHandler async.PanicHan // GetUserIDs returns the user IDs and usernames of all users in the vault. func (vault *Vault) GetUserIDs() []string { - return xslices.Map(vault.get().Users, func(user UserData) string { + vault.lock.RLock() + defer vault.lock.RUnlock() + + return xslices.Map(vault.getUnsafe().Users, func(user UserData) string { return user.UserID }) } +func (vault *Vault) getUsers() ([]*User, error) { + vault.lock.Lock() + defer vault.lock.Unlock() + + users := vault.getUnsafe().Users + + result := make([]*User, 0, len(users)) + + for _, user := range users { + u, err := vault.newUserUnsafe(user.UserID) + if err != nil { + for _, v := range result { + if err := v.Close(); err != nil { + logrus.WithError(err).Error("Fait to close user after failed get") + } + } + + return nil, err + } + + result = append(result, u) + } + + return result, nil +} + // HasUser returns true if the vault contains a user with the given ID. func (vault *Vault) HasUser(userID string) bool { - return xslices.IndexFunc(vault.get().Users, func(user UserData) bool { + vault.lock.RLock() + defer vault.lock.RUnlock() + + return xslices.IndexFunc(vault.getUnsafe().Users, func(user UserData) bool { return user.UserID == userID }) >= 0 } @@ -106,41 +138,61 @@ func (vault *Vault) GetUser(userID string, fn func(*User)) error { // NewUser returns a new vault user. It must be closed before it can be deleted. func (vault *Vault) NewUser(userID string) (*User, error) { - if idx := xslices.IndexFunc(vault.get().Users, func(user UserData) bool { + vault.lock.Lock() + defer vault.lock.Unlock() + + return vault.newUserUnsafe(userID) +} + +func (vault *Vault) newUserUnsafe(userID string) (*User, error) { + if idx := xslices.IndexFunc(vault.getUnsafe().Users, func(user UserData) bool { return user.UserID == userID }); idx < 0 { return nil, errors.New("no such user") } - return vault.attachUser(userID), nil + return vault.attachUserUnsafe(userID), nil } // ForUser executes a callback for each user in the vault. func (vault *Vault) ForUser(parallelism int, fn func(*User) error) error { - userIDs := vault.GetUserIDs() + users, err := vault.getUsers() + if err != nil { + return err + } - return parallel.DoContext(context.Background(), parallelism, len(userIDs), func(_ context.Context, idx int) error { + r := parallel.DoContext(context.Background(), parallelism, len(users), func(_ context.Context, idx int) error { defer async.HandlePanic(vault.panicHandler) - user, err := vault.NewUser(userIDs[idx]) - if err != nil { - return err - } - defer func() { _ = user.Close() }() - + user := users[idx] return fn(user) }) + + for _, u := range users { + if err := u.Close(); err != nil { + logrus.WithError(err).Error("Failed to close user after ForUser") + } + } + + return r } // AddUser creates a new user in the vault with the given ID, username and password. // A gluon key is generated using the package's token generator. If a password is found in the password archive for this user, // it is restored, otherwise a new bridge password is generated using the package's token generator. func (vault *Vault) AddUser(userID, username, primaryEmail, authUID, authRef string, keyPass []byte) (*User, error) { + vault.lock.Lock() + defer vault.lock.Unlock() + + return vault.addUserUnsafe(userID, username, primaryEmail, authUID, authRef, keyPass) +} + +func (vault *Vault) addUserUnsafe(userID, username, primaryEmail, authUID, authRef string, keyPass []byte) (*User, error) { logrus.WithField("userID", userID).Info("Adding vault user") var exists bool - if err := vault.mod(func(data *Data) { + if err := vault.modUnsafe(func(data *Data) { if idx := xslices.IndexFunc(data.Users, func(user UserData) bool { return user.UserID == userID }); idx >= 0 { @@ -161,13 +213,42 @@ func (vault *Vault) AddUser(userID, username, primaryEmail, authUID, authRef str return nil, errors.New("user already exists") } - return vault.NewUser(userID) + return vault.attachUserUnsafe(userID), nil +} + +// GetOrAddUser retrieves an existing user and updates the authRef and keyPass or creates a new user. Returns +// the user and whether the user did not exist before. +func (vault *Vault) GetOrAddUser(userID, username, primaryEmail, authUID, authRef string, keyPass []byte) (*User, bool, error) { + vault.lock.Lock() + defer vault.lock.Unlock() + + { + users := vault.getUnsafe().Users + + idx := xslices.IndexFunc(users, func(user UserData) bool { + return user.UserID == userID + }) + + if idx >= 0 { + user := vault.attachUserUnsafe(userID) + + if err := user.setAuthAndKeyPassUnsafe(authUID, authRef, keyPass); err != nil { + return nil, false, err + } + + return user, false, nil + } + } + + u, err := vault.addUserUnsafe(userID, username, primaryEmail, authUID, authRef, keyPass) + + return u, true, err } // DeleteUser removes the given user from the vault. func (vault *Vault) DeleteUser(userID string) error { - vault.refLock.Lock() - defer vault.refLock.Unlock() + vault.lock.Lock() + defer vault.lock.Unlock() logrus.WithField("userID", userID).Info("Deleting vault user") @@ -175,7 +256,7 @@ func (vault *Vault) DeleteUser(userID string) error { return fmt.Errorf("user %s is currently in use", userID) } - return vault.mod(func(data *Data) { + return vault.modUnsafe(func(data *Data) { idx := xslices.IndexFunc(data.Users, func(user UserData) bool { return user.UserID == userID }) @@ -189,17 +270,26 @@ func (vault *Vault) DeleteUser(userID string) error { } func (vault *Vault) Migrated() bool { - return vault.get().Migrated + vault.lock.RLock() + defer vault.lock.RUnlock() + + return vault.getUnsafe().Migrated } func (vault *Vault) SetMigrated() error { - return vault.mod(func(data *Data) { + vault.lock.Lock() + defer vault.lock.Unlock() + + return vault.modUnsafe(func(data *Data) { data.Migrated = true }) } func (vault *Vault) Reset(gluonDir string) error { - return vault.mod(func(data *Data) { + vault.lock.Lock() + defer vault.lock.Unlock() + + return vault.modUnsafe(func(data *Data) { *data = newDefaultData(gluonDir) }) } @@ -209,8 +299,8 @@ func (vault *Vault) Path() string { } func (vault *Vault) Close() error { - vault.refLock.Lock() - defer vault.refLock.Unlock() + vault.lock.Lock() + defer vault.lock.Unlock() if len(vault.ref) > 0 { return errors.New("vault is still in use") @@ -221,10 +311,7 @@ func (vault *Vault) Close() error { return nil } -func (vault *Vault) attachUser(userID string) *User { - vault.refLock.Lock() - defer vault.refLock.Unlock() - +func (vault *Vault) attachUserUnsafe(userID string) *User { logrus.WithField("userID", userID).Trace("Attaching vault user") vault.ref[userID]++ @@ -236,8 +323,8 @@ func (vault *Vault) attachUser(userID string) *User { } func (vault *Vault) detachUser(userID string) error { - vault.refLock.Lock() - defer vault.refLock.Unlock() + vault.lock.Lock() + defer vault.lock.Unlock() logrus.WithField("userID", userID).Trace("Detaching vault user") @@ -289,10 +376,14 @@ func newVault(path, gluonDir string, gcm cipher.AEAD) (*Vault, bool, error) { }, corrupt, nil } -func (vault *Vault) get() Data { - vault.encLock.RLock() - defer vault.encLock.RUnlock() +func (vault *Vault) getSafe() Data { + vault.lock.RLock() + defer vault.lock.RUnlock() + return vault.getUnsafe() +} + +func (vault *Vault) getUnsafe() Data { var data Data if err := unmarshalFile(vault.gcm, vault.enc, &data); err != nil { @@ -302,10 +393,14 @@ func (vault *Vault) get() Data { return data } -func (vault *Vault) mod(fn func(data *Data)) error { - vault.encLock.Lock() - defer vault.encLock.Unlock() +func (vault *Vault) modSafe(fn func(data *Data)) error { + vault.lock.Lock() + defer vault.lock.Unlock() + return vault.modUnsafe(fn) +} + +func (vault *Vault) modUnsafe(fn func(data *Data)) error { var data Data if err := unmarshalFile(vault.gcm, vault.enc, &data); err != nil { @@ -325,13 +420,31 @@ func (vault *Vault) mod(fn func(data *Data)) error { } func (vault *Vault) getUser(userID string) UserData { - return vault.get().Users[xslices.IndexFunc(vault.get().Users, func(user UserData) bool { + vault.lock.RLock() + defer vault.lock.RUnlock() + + users := vault.getUnsafe().Users + + idx := xslices.IndexFunc(users, func(user UserData) bool { return user.UserID == userID - })] + }) + + if idx < 0 { + panic("Unknown user") + } + + return users[idx] } func (vault *Vault) modUser(userID string, fn func(userData *UserData)) error { - return vault.mod(func(data *Data) { + vault.lock.Lock() + defer vault.lock.Unlock() + + return vault.modUserUnsafe(userID, fn) +} + +func (vault *Vault) modUserUnsafe(userID string, fn func(userData *UserData)) error { + return vault.modUnsafe(func(data *Data) { idx := xslices.IndexFunc(data.Users, func(user UserData) bool { return user.UserID == userID }) diff --git a/internal/vault/vault_debug.go b/internal/vault/vault_debug.go index 17ece7f8..aef5f5cf 100644 --- a/internal/vault/vault_debug.go +++ b/internal/vault/vault_debug.go @@ -24,7 +24,7 @@ import ( ) func (vault *Vault) ImportJSON(dec []byte) { - vault.mod(func(data *Data) { + vault.modSafe(func(data *Data) { if err := json.Unmarshal(dec, data); err != nil { panic(err) } @@ -32,7 +32,7 @@ func (vault *Vault) ImportJSON(dec []byte) { } func (vault *Vault) ExportJSON() []byte { - enc, err := json.MarshalIndent(vault.get(), "", " ") + enc, err := json.MarshalIndent(vault.getSafe(), "", " ") if err != nil { panic(err) } From 8c958cdc2f673237a7f62a238aa578792255f7ef Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Tue, 16 May 2023 09:34:45 +0200 Subject: [PATCH 29/43] chore: Bump Gluon for GODT-2595, GODT-2634 and GODT-2619 https://github.com/ProtonMail/gluon/pull/350 https://github.com/ProtonMail/gluon/pull/351 https://github.com/ProtonMail/gluon/pull/352 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 465b3829..158c41fb 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 github.com/Masterminds/semver/v3 v3.2.0 - github.com/ProtonMail/gluon v0.16.1-0.20230508105645-e4f4a844ccae + github.com/ProtonMail/gluon v0.16.1-0.20230516073349-d18e5932b28f github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a github.com/ProtonMail/go-proton-api v0.4.1-0.20230505091503-167f3d239b0c github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton diff --git a/go.sum b/go.sum index 4a7c0b88..15e57690 100644 --- a/go.sum +++ b/go.sum @@ -28,8 +28,8 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo= github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk= github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g= -github.com/ProtonMail/gluon v0.16.1-0.20230508105645-e4f4a844ccae h1:3p8P21+BoAYj1nSswdwQvc7jr2lixuVFpWE4QlvA8f0= -github.com/ProtonMail/gluon v0.16.1-0.20230508105645-e4f4a844ccae/go.mod h1:yA4hk6CJw0BMo+YL8Y3ckCYs5L20sysu9xseshwY3QI= +github.com/ProtonMail/gluon v0.16.1-0.20230516073349-d18e5932b28f h1:z01cLLqPBrS9mHshUgE5444Qpcl0Cpz96ldfqxRTseU= +github.com/ProtonMail/gluon v0.16.1-0.20230516073349-d18e5932b28f/go.mod h1:yA4hk6CJw0BMo+YL8Y3ckCYs5L20sysu9xseshwY3QI= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= From 4e3ad4f7fad565db21d88e0586d51c25bd5c0c13 Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Tue, 16 May 2023 09:53:41 +0200 Subject: [PATCH 30/43] fix(GODT-2626): Server Events should not be merged. d18e5932b28f83b201709a04fb7b8c6f74003574 Includes GPA bump: https://github.com/ProtonMail/go-proton-api/pull/80 --- go.mod | 2 +- go.sum | 2 + internal/user/user.go | 122 +++++++++++++++++++++--------------------- 3 files changed, 65 insertions(+), 61 deletions(-) diff --git a/go.mod b/go.mod index 158c41fb..2712e430 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.2.0 github.com/ProtonMail/gluon v0.16.1-0.20230516073349-d18e5932b28f github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a - github.com/ProtonMail/go-proton-api v0.4.1-0.20230505091503-167f3d239b0c + github.com/ProtonMail/go-proton-api v0.4.1-0.20230516070548-faf4f87bf9e7 github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton github.com/PuerkitoBio/goquery v1.8.1 github.com/abiosoft/ishell v2.0.0+incompatible diff --git a/go.sum b/go.sum index 15e57690..7b03b86b 100644 --- a/go.sum +++ b/go.sum @@ -41,6 +41,8 @@ github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ek github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= github.com/ProtonMail/go-proton-api v0.4.1-0.20230505091503-167f3d239b0c h1:uqo3mKt4ffhqPFLVV7VxjuN12DAFQmqEju/Wy5dk6Rk= github.com/ProtonMail/go-proton-api v0.4.1-0.20230505091503-167f3d239b0c/go.mod h1:UkrG9gN2o9mzdx/an0XRc6a4s5Haef1A7Eyd2iXlw28= +github.com/ProtonMail/go-proton-api v0.4.1-0.20230516070548-faf4f87bf9e7 h1:7aY4azqc8PzYtg4+xG7b9wBEnckrl7rVMlMoFMWRkdA= +github.com/ProtonMail/go-proton-api v0.4.1-0.20230516070548-faf4f87bf9e7/go.mod h1:UkrG9gN2o9mzdx/an0XRc6a4s5Haef1A7Eyd2iXlw28= github.com/ProtonMail/go-srp v0.0.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg= github.com/ProtonMail/go-srp v0.0.5/go.mod h1:06iYHtLXW8vjLtccWj++x3MKy65sIT8yZd7nrJF49rs= github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton h1:YS6M20yvjCJPR1r4ADW5TPn6rahs4iAyZaACei86bEc= diff --git a/internal/user/user.go b/internal/user/user.go index 3e601b9c..8376fb61 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -690,87 +690,89 @@ func (user *User) doEventPoll(ctx context.Context) error { user.eventLock.Lock() defer user.eventLock.Unlock() - event, more, err := user.client.GetEvent(ctx, user.vault.EventID()) + gpaEvents, more, err := user.client.GetEvent(ctx, user.vault.EventID()) if err != nil { return fmt.Errorf("failed to get event (caused by %T): %w", internal.ErrCause(err), err) } // If the event ID hasn't changed, there are no new events. - if event.EventID == user.vault.EventID() { + if gpaEvents[len(gpaEvents)-1].EventID == user.vault.EventID() { user.log.Debug("No new API events") return nil } - user.log.WithFields(logrus.Fields{ - "old": user.vault.EventID(), - "new": event, - }).Info("Received new API event") + for _, event := range gpaEvents { + user.log.WithFields(logrus.Fields{ + "old": user.vault.EventID(), + "new": event, + }).Info("Received new API event") - // Handle the event. - if err := user.handleAPIEvent(ctx, event); err != nil { - // If the error is a context cancellation, return error to retry later. - if errors.Is(err, context.Canceled) { - return fmt.Errorf("failed to handle event due to context cancellation: %w", err) + // Handle the event. + if err := user.handleAPIEvent(ctx, event); err != nil { + // If the error is a context cancellation, return error to retry later. + if errors.Is(err, context.Canceled) { + return fmt.Errorf("failed to handle event due to context cancellation: %w", err) + } + + // If the error is a network error, return error to retry later. + if netErr := new(proton.NetError); errors.As(err, &netErr) { + return fmt.Errorf("failed to handle event due to network issue: %w", err) + } + + // Catch all for uncategorized net errors that may slip through. + if netErr := new(net.OpError); errors.As(err, &netErr) { + return fmt.Errorf("failed to handle event due to network issues (uncategorized): %w", err) + } + + // In case a json decode error slips through. + if jsonErr := new(json.UnmarshalTypeError); errors.As(err, &jsonErr) { + user.eventCh.Enqueue(events.UncategorizedEventError{ + UserID: user.ID(), + Error: err, + }) + + return fmt.Errorf("failed to handle event due to JSON issue: %w", err) + } + + // If the error is an unexpected EOF, return error to retry later. + if errors.Is(err, io.ErrUnexpectedEOF) { + return fmt.Errorf("failed to handle event due to EOF: %w", err) + } + + // If the error is a server-side issue, return error to retry later. + if apiErr := new(proton.APIError); errors.As(err, &apiErr) && apiErr.Status >= 500 { + return fmt.Errorf("failed to handle event due to server error: %w", err) + } + + // Otherwise, the error is a client-side issue; notify bridge to handle it. + user.log.WithField("event", event).Warn("Failed to handle API event") + + user.eventCh.Enqueue(events.UserBadEvent{ + UserID: user.ID(), + OldEventID: user.vault.EventID(), + NewEventID: event.EventID, + EventInfo: event.String(), + Error: err, + }) + + return fmt.Errorf("failed to handle event due to client error: %w", err) } - // If the error is a network error, return error to retry later. - if netErr := new(proton.NetError); errors.As(err, &netErr) { - return fmt.Errorf("failed to handle event due to network issue: %w", err) - } + user.log.WithField("event", event).Debug("Handled API event") - // Catch all for uncategorized net errors that may slip through. - if netErr := new(net.OpError); errors.As(err, &netErr) { - return fmt.Errorf("failed to handle event due to network issues (uncategorized): %w", err) - } - - // In case a json decode error slips through. - if jsonErr := new(json.UnmarshalTypeError); errors.As(err, &jsonErr) { - user.eventCh.Enqueue(events.UncategorizedEventError{ + // Update the event ID in the vault. If this fails, notify bridge to handle it. + if err := user.vault.SetEventID(event.EventID); err != nil { + user.eventCh.Enqueue(events.UserBadEvent{ UserID: user.ID(), Error: err, }) - return fmt.Errorf("failed to handle event due to JSON issue: %w", err) + return fmt.Errorf("failed to update event ID: %w", err) } - // If the error is an unexpected EOF, return error to retry later. - if errors.Is(err, io.ErrUnexpectedEOF) { - return fmt.Errorf("failed to handle event due to EOF: %w", err) - } - - // If the error is a server-side issue, return error to retry later. - if apiErr := new(proton.APIError); errors.As(err, &apiErr) && apiErr.Status >= 500 { - return fmt.Errorf("failed to handle event due to server error: %w", err) - } - - // Otherwise, the error is a client-side issue; notify bridge to handle it. - user.log.WithField("event", event).Warn("Failed to handle API event") - - user.eventCh.Enqueue(events.UserBadEvent{ - UserID: user.ID(), - OldEventID: user.vault.EventID(), - NewEventID: event.EventID, - EventInfo: event.String(), - Error: err, - }) - - return fmt.Errorf("failed to handle event due to client error: %w", err) + user.log.WithField("eventID", event.EventID).Debug("Updated event ID in vault") } - user.log.WithField("event", event).Debug("Handled API event") - - // Update the event ID in the vault. If this fails, notify bridge to handle it. - if err := user.vault.SetEventID(event.EventID); err != nil { - user.eventCh.Enqueue(events.UserBadEvent{ - UserID: user.ID(), - Error: err, - }) - - return fmt.Errorf("failed to update event ID: %w", err) - } - - user.log.WithField("eventID", event.EventID).Debug("Updated event ID in vault") - if more { user.goPollAPIEvents(false) } From d8ccc6c05d132010afd94048042229410e43aa57 Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Tue, 16 May 2023 10:41:36 +0200 Subject: [PATCH 31/43] fix(GODT-2626): Handle rare crash due to missing address update ch Ensure that we can handle the rare case that can cause a crash if for whichever reason we end up with an Address Delete and Message Create/Update in the same event object. --- internal/user/events.go | 69 ++++++++++++++++++++++++++++++++++++++--- internal/user/types.go | 12 +++++++ 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/internal/user/events.go b/internal/user/events.go index 760d678d..b848a98e 100644 --- a/internal/user/events.go +++ b/internal/user/events.go @@ -217,7 +217,7 @@ func (user *User) handleCreateAddressEvent(ctx context.Context, event proton.Add // If the address is enabled, we need to hook it up to the update channels. switch user.vault.AddressMode() { case vault.CombinedMode: - primAddr, err := getAddrIdx(user.apiAddrs, 0) + primAddr, err := getPrimaryAddr(user.apiAddrs) if err != nil { return fmt.Errorf("failed to get primary address: %w", err) } @@ -276,7 +276,7 @@ func (user *User) handleUpdateAddressEvent(_ context.Context, event proton.Addre case oldAddr.Status != proton.AddressStatusEnabled && event.Address.Status == proton.AddressStatusEnabled: switch user.vault.AddressMode() { case vault.CombinedMode: - primAddr, err := getAddrIdx(user.apiAddrs, 0) + primAddr, err := getPrimaryAddr(user.apiAddrs) if err != nil { return fmt.Errorf("failed to get primary address: %w", err) } @@ -628,7 +628,14 @@ func (user *User) handleCreateMessageEvent(ctx context.Context, message proton.M } update = imap.NewMessagesCreated(false, res.update) - user.updateCh[full.AddressID].Enqueue(update) + didPublish, err := safePublishMessageUpdate(user, full.AddressID, update) + if err != nil { + return err + } + + if !didPublish { + update = nil + } return nil }); err != nil { @@ -674,7 +681,14 @@ func (user *User) handleUpdateMessageEvent(_ context.Context, message proton.Mes flags, ) - user.updateCh[message.AddressID].Enqueue(update) + didPublish, err := safePublishMessageUpdate(user, message.AddressID, update) + if err != nil { + return nil, err + } + + if !didPublish { + return nil, nil + } return []imap.Update{update}, nil }, user.apiLabelsLock, user.updateChLock) @@ -743,13 +757,24 @@ func (user *User) handleUpdateDraftEvent(ctx context.Context, event proton.Messa true, // Is the message doesn't exist, silently create it. ) - user.updateCh[full.AddressID].Enqueue(update) + didPublish, err := safePublishMessageUpdate(user, full.AddressID, update) + if err != nil { + return err + } + + if !didPublish { + update = nil + } return nil }); err != nil { return nil, err } + if update == nil { + return nil, nil + } + return []imap.Update{update}, nil }, user.apiUserLock, user.apiAddrsLock, user.apiLabelsLock, user.updateChLock) } @@ -816,3 +841,37 @@ func (user *User) reportErrorNoContextCancel(title string, err error, reportCont } } } + +// safePublishMessageUpdate handles the rare case where the address' update channel may have been deleted in the same +// event. This rare case can take place if in the same event fetch request there is an update for delete address and +// create/update message. +// If the user is in combined mode, we simply push the update to the primary address. If the user is in split mode +// we do not publish the update as the address no longer exists. +func safePublishMessageUpdate(user *User, addressID string, update imap.Update) (bool, error) { + v, ok := user.updateCh[addressID] + if !ok { + if user.GetAddressMode() == vault.CombinedMode { + primAddr, err := getPrimaryAddr(user.apiAddrs) + if err != nil { + return false, fmt.Errorf("failed to get primary address: %w", err) + } + primaryCh, ok := user.updateCh[primAddr.ID] + if !ok { + return false, fmt.Errorf("primary address channel is not available") + } + + primaryCh.Enqueue(update) + + return true, nil + } + + logrus.Warnf("Update channel not found for address %v, it may have been already deleted", addressID) + _ = user.reporter.ReportMessage("Message Update channel does not exist") + + return false, nil + } + + v.Enqueue(update) + + return true, nil +} diff --git a/internal/user/types.go b/internal/user/types.go index fc6a2286..67fbdf04 100644 --- a/internal/user/types.go +++ b/internal/user/types.go @@ -83,6 +83,18 @@ func getAddrIdx(apiAddrs map[string]proton.Address, idx int) (proton.Address, er return sorted[idx], nil } +func getPrimaryAddr(apiAddrs map[string]proton.Address) (proton.Address, error) { + sorted := sortSlice(maps.Values(apiAddrs), func(a, b proton.Address) bool { + return a.Order < b.Order + }) + + if len(sorted) == 0 { + return proton.Address{}, fmt.Errorf("no addresses available") + } + + return sorted[0], nil +} + // sortSlice returns the given slice sorted by the given comparator. func sortSlice[Item any](items []Item, less func(Item, Item) bool) []Item { sorted := make([]Item, len(items)) From 9fc9f5ad9fbffcab0842caa87bf9172bc79eaad8 Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Tue, 16 May 2023 11:25:11 +0200 Subject: [PATCH 32/43] fix(GODT-2635): Ensure Bridge can be compiled with GCC 13 Requires updating vcpkg to include the port fixes. --- extern/vcpkg | 2 +- .../bridgepp/FocusGRPC/focus.grpc.pb.cc | 24 ++++++++-------- .../bridgepp/FocusGRPC/focus.grpc.pb.h | 28 +++++++++---------- .../bridgepp/bridgepp/FocusGRPC/focus.pb.h | 2 +- .../bridgepp/bridgepp/GRPC/bridge.grpc.pb.cc | 24 ++++++++-------- .../bridgepp/bridgepp/GRPC/bridge.grpc.pb.h | 28 +++++++++---------- .../bridgepp/bridgepp/GRPC/bridge.pb.h | 2 +- 7 files changed, 55 insertions(+), 55 deletions(-) diff --git a/extern/vcpkg b/extern/vcpkg index f93ba152..d4d39d71 160000 --- a/extern/vcpkg +++ b/extern/vcpkg @@ -1 +1 @@ -Subproject commit f93ba152d55e1d243160e690bc302ffe8638358e +Subproject commit d4d39d71b3e6dd7536592c36ab2f7e84a8a64942 diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/FocusGRPC/focus.grpc.pb.cc b/internal/frontend/bridge-gui/bridgepp/bridgepp/FocusGRPC/focus.grpc.pb.cc index 55d3e961..68bc4e56 100644 --- a/internal/frontend/bridge-gui/bridgepp/bridgepp/FocusGRPC/focus.grpc.pb.cc +++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/FocusGRPC/focus.grpc.pb.cc @@ -6,19 +6,19 @@ #include "focus.grpc.pb.h" #include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include -#include -#include -#include +#include +#include +#include namespace focus { static const char* Focus_method_names[] = { diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/FocusGRPC/focus.grpc.pb.h b/internal/frontend/bridge-gui/bridgepp/bridgepp/FocusGRPC/focus.grpc.pb.h index be4b3441..bc59c52d 100644 --- a/internal/frontend/bridge-gui/bridgepp/bridgepp/FocusGRPC/focus.grpc.pb.h +++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/FocusGRPC/focus.grpc.pb.h @@ -25,23 +25,23 @@ #include "focus.pb.h" #include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include #include -#include -#include +#include +#include #include -#include -#include +#include +#include #include -#include -#include +#include +#include namespace focus { diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/FocusGRPC/focus.pb.h b/internal/frontend/bridge-gui/bridgepp/bridgepp/FocusGRPC/focus.pb.h index 53a7184f..cd281b7f 100644 --- a/internal/frontend/bridge-gui/bridgepp/bridgepp/FocusGRPC/focus.pb.h +++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/FocusGRPC/focus.pb.h @@ -13,7 +13,7 @@ #error incompatible with your Protocol Buffer headers. Please update #error your headers. #endif -#if 3021003 < PROTOBUF_MIN_PROTOC_VERSION +#if 3021012 < PROTOBUF_MIN_PROTOC_VERSION #error This file was generated by an older version of protoc which is #error incompatible with your Protocol Buffer headers. Please #error regenerate this file with a newer version of protoc. diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/bridge.grpc.pb.cc b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/bridge.grpc.pb.cc index 53beac31..a032ba05 100644 --- a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/bridge.grpc.pb.cc +++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/bridge.grpc.pb.cc @@ -6,19 +6,19 @@ #include "bridge.grpc.pb.h" #include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include -#include -#include -#include +#include +#include +#include namespace grpc { static const char* Bridge_method_names[] = { diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/bridge.grpc.pb.h b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/bridge.grpc.pb.h index 86387cc3..092e37b5 100644 --- a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/bridge.grpc.pb.h +++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/bridge.grpc.pb.h @@ -25,23 +25,23 @@ #include "bridge.pb.h" #include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include #include -#include -#include +#include +#include #include -#include -#include +#include +#include #include -#include -#include +#include +#include namespace grpc { diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/bridge.pb.h b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/bridge.pb.h index ea2f44df..b32f4612 100644 --- a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/bridge.pb.h +++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/bridge.pb.h @@ -13,7 +13,7 @@ #error incompatible with your Protocol Buffer headers. Please update #error your headers. #endif -#if 3021003 < PROTOBUF_MIN_PROTOC_VERSION +#if 3021012 < PROTOBUF_MIN_PROTOC_VERSION #error This file was generated by an older version of protoc which is #error incompatible with your Protocol Buffer headers. Please #error regenerate this file with a newer version of protoc. From 900caec09edb69d3095f784c40c226081f2f1ef9 Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Tue, 16 May 2023 16:06:49 +0200 Subject: [PATCH 33/43] fix(GODT-2637): Fix address parser error due to trailing separator https://github.com/ProtonMail/gluon/pull/353 --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 2712e430..c9a49874 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 github.com/Masterminds/semver/v3 v3.2.0 - github.com/ProtonMail/gluon v0.16.1-0.20230516073349-d18e5932b28f + github.com/ProtonMail/gluon v0.16.1-0.20230516135940-edc685bb7ebb github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a github.com/ProtonMail/go-proton-api v0.4.1-0.20230516070548-faf4f87bf9e7 github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton diff --git a/go.sum b/go.sum index 7b03b86b..76605247 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,8 @@ github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkF github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g= github.com/ProtonMail/gluon v0.16.1-0.20230516073349-d18e5932b28f h1:z01cLLqPBrS9mHshUgE5444Qpcl0Cpz96ldfqxRTseU= github.com/ProtonMail/gluon v0.16.1-0.20230516073349-d18e5932b28f/go.mod h1:yA4hk6CJw0BMo+YL8Y3ckCYs5L20sysu9xseshwY3QI= +github.com/ProtonMail/gluon v0.16.1-0.20230516135940-edc685bb7ebb h1:LigybrCpBqujX48/K3y8v5pdtYCDmOwHD+H0liIS0P4= +github.com/ProtonMail/gluon v0.16.1-0.20230516135940-edc685bb7ebb/go.mod h1:yA4hk6CJw0BMo+YL8Y3ckCYs5L20sysu9xseshwY3QI= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= From 21f833ea1192955ab54a66c6b05e6c22635e1ee6 Mon Sep 17 00:00:00 2001 From: Xavier Michelon Date: Wed, 17 May 2023 09:45:31 +0200 Subject: [PATCH 34/43] fix(GODT-2307): removed deprecated macOS security framework function. --- go.mod | 2 +- go.sum | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index c9a49874..4f04d632 100644 --- a/go.mod +++ b/go.mod @@ -127,5 +127,5 @@ require ( replace ( github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0 github.com/emersion/go-message => github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753 - github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe + github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768 ) diff --git a/go.sum b/go.sum index 76605247..48716930 100644 --- a/go.sum +++ b/go.sum @@ -41,8 +41,6 @@ github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753 h1:I8IsYA297 github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= -github.com/ProtonMail/go-proton-api v0.4.1-0.20230505091503-167f3d239b0c h1:uqo3mKt4ffhqPFLVV7VxjuN12DAFQmqEju/Wy5dk6Rk= -github.com/ProtonMail/go-proton-api v0.4.1-0.20230505091503-167f3d239b0c/go.mod h1:UkrG9gN2o9mzdx/an0XRc6a4s5Haef1A7Eyd2iXlw28= github.com/ProtonMail/go-proton-api v0.4.1-0.20230516070548-faf4f87bf9e7 h1:7aY4azqc8PzYtg4+xG7b9wBEnckrl7rVMlMoFMWRkdA= github.com/ProtonMail/go-proton-api v0.4.1-0.20230516070548-faf4f87bf9e7/go.mod h1:UkrG9gN2o9mzdx/an0XRc6a4s5Haef1A7Eyd2iXlw28= github.com/ProtonMail/go-srp v0.0.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg= @@ -110,8 +108,8 @@ github.com/cucumber/godog v0.12.5/go.mod h1:u6SD7IXC49dLpPN35kal0oYEjsXZWee4pW6T github.com/cucumber/messages-go/v16 v16.0.0/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g= github.com/cucumber/messages-go/v16 v16.0.1 h1:fvkpwsLgnIm0qugftrw2YwNlio+ABe2Iu94Ap8GMYIY= github.com/cucumber/messages-go/v16 v16.0.1/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g= -github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe h1:KRj3wdvA9yE92prNmOjS7x5DOqoyjxqdE30qnrmTasc= -github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe/go.mod h1:ZoZU1fnBy3mOLWr3Pg+Y2+nTKtu6ypDte2kZg9HvSwY= +github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768 h1:Jrcoxtrk4qpuzKIYPlEkjIK0M+bABs0oW2QzrOuwlzk= +github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768/go.mod h1:ZoZU1fnBy3mOLWr3Pg+Y2+nTKtu6ypDte2kZg9HvSwY= github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= From d6304b087aed7fb8c8d97bb5791f5fb9657249fe Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Tue, 16 May 2023 16:51:20 +0200 Subject: [PATCH 35/43] fix(GODT-2627): Fix crash on closed channel See `TestSendHasher_DualAddDoesNotCauseCrash`'s comment for more details. --- internal/user/send_recorder.go | 21 +++++++++++++++------ internal/user/send_recorder_test.go | 26 +++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/internal/user/send_recorder.go b/internal/user/send_recorder.go index fae307e0..228bbfe6 100644 --- a/internal/user/send_recorder.go +++ b/internal/user/send_recorder.go @@ -46,10 +46,18 @@ func newSendRecorder(expiry time.Duration) *sendRecorder { } type sendEntry struct { - msgID string - toList []string - exp time.Time - waitCh chan struct{} + msgID string + toList []string + exp time.Time + waitCh chan struct{} + waitChClosed bool +} + +func (s *sendEntry) closeWaitChannel() { + if !s.waitChClosed { + close(s.waitCh) + s.waitChClosed = true + } } // tryInsertWait tries to insert the given message into the send recorder. @@ -142,6 +150,7 @@ func (h *sendRecorder) hasEntry(hash string) bool { return false } +// addMessageID should be called after a message has been successfully sent. func (h *sendRecorder) addMessageID(hash, msgID string) { h.entriesLock.Lock() defer h.entriesLock.Unlock() @@ -153,7 +162,7 @@ func (h *sendRecorder) addMessageID(hash, msgID string) { logrus.Warn("Cannot add message ID to send hash entry, it may have expired") } - close(entry.waitCh) + entry.closeWaitChannel() } func (h *sendRecorder) removeOnFail(hash string) { @@ -165,7 +174,7 @@ func (h *sendRecorder) removeOnFail(hash string) { return } - close(entry.waitCh) + entry.closeWaitChannel() delete(h.entries, hash) } diff --git a/internal/user/send_recorder_test.go b/internal/user/send_recorder_test.go index 075a3b52..0aabe90b 100644 --- a/internal/user/send_recorder_test.go +++ b/internal/user/send_recorder_test.go @@ -194,6 +194,31 @@ func TestSendHasher_HasEntry_SendSuccess(t *testing.T) { require.Equal(t, "abc", messageID) } +func TestSendHasher_DualAddDoesNotCauseCrash(t *testing.T) { + // There may be a rare case where one 2 smtp connections attempt to send the same message, but if the first message + // is stuck long enough for it to expire, the second connection will remove it from the list and cause it to be + // inserted as a new entry. The two clients end up sending the message twice and calling the `addMessageID` x2, + // resulting in a crash. + h := newSendRecorder(sendEntryExpiry) + + // Insert a message into the hasher. + hash, ok, err := testTryInsert(h, literal1, time.Now().Add(time.Second)) + require.NoError(t, err) + require.True(t, ok) + require.NotEmpty(t, hash) + + // Simulate successfully sending the message. We call this method twice as it possible for multiple SMTP connections + // to attempt to send the same message. + h.addMessageID(hash, "abc") + h.addMessageID(hash, "abc") + + // The message was already sent; we should find it in the hasher. + messageID, ok, err := testHasEntry(h, literal1, time.Now().Add(time.Second)) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, "abc", messageID) +} + func TestSendHasher_HasEntry_SendFail(t *testing.T) { h := newSendRecorder(sendEntryExpiry) @@ -264,7 +289,6 @@ Content-Disposition: attachment; filename="attname.txt" attachment --longrandomstring-- ` - const literal2 = `From: Sender To: Receiver Content-Type: multipart/mixed; boundary=longrandomstring From 5fee2f707be2d26f92fffb3de3c016aa96431c23 Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Tue, 16 May 2023 17:37:25 +0200 Subject: [PATCH 36/43] fix(GODT-2627): Properly handle recording of message with Bcc fields Ensure the SMTP send recorder properly handles the recording of messages which may have the same body hash but have different recipients. E.g.: send the same message twice to 2 different users via Bcc. The send recorder now maintains a list of send requests and waiting for a message to be sent is done one the oldest of the messages. --- internal/user/send_recorder.go | 85 +++++++++++++++++++---------- internal/user/send_recorder_test.go | 38 +++++++++---- internal/user/smtp.go | 4 +- 3 files changed, 84 insertions(+), 43 deletions(-) diff --git a/internal/user/send_recorder.go b/internal/user/send_recorder.go index 228bbfe6..306f1384 100644 --- a/internal/user/send_recorder.go +++ b/internal/user/send_recorder.go @@ -25,6 +25,7 @@ import ( "time" "github.com/ProtonMail/gluon/rfc822" + "github.com/bradenaw/juniper/xslices" "github.com/sirupsen/logrus" "golang.org/x/exp/slices" ) @@ -34,14 +35,14 @@ const sendEntryExpiry = 30 * time.Minute type sendRecorder struct { expiry time.Duration - entries map[string]*sendEntry + entries map[string][]*sendEntry entriesLock sync.Mutex } func newSendRecorder(expiry time.Duration) *sendRecorder { return &sendRecorder{ expiry: expiry, - entries: make(map[string]*sendEntry), + entries: make(map[string][]*sendEntry), } } @@ -110,25 +111,40 @@ func (h *sendRecorder) hasEntryWait(ctx context.Context, hash string, deadline t return h.hasEntryWait(ctx, hash, deadline) } +func (h *sendRecorder) removeExpiredUnsafe() { + for hash, entry := range h.entries { + remaining := xslices.Filter(entry, func(t *sendEntry) bool { + return !t.exp.Before(time.Now()) + }) + + if len(remaining) == 0 { + delete(h.entries, hash) + } else { + h.entries[hash] = remaining + } + } +} + func (h *sendRecorder) tryInsert(hash string, toList []string) bool { h.entriesLock.Lock() defer h.entriesLock.Unlock() - for hash, entry := range h.entries { - if entry.exp.Before(time.Now()) { - delete(h.entries, hash) + h.removeExpiredUnsafe() + + entries, ok := h.entries[hash] + if ok { + for _, entry := range entries { + if matchToList(entry.toList, toList) { + return false + } } } - if _, ok := h.entries[hash]; ok && matchToList(h.entries[hash].toList, toList) { - return false - } - - h.entries[hash] = &sendEntry{ + h.entries[hash] = append(entries, &sendEntry{ exp: time.Now().Add(h.expiry), toList: toList, waitCh: make(chan struct{}), - } + }) return true } @@ -137,11 +153,7 @@ func (h *sendRecorder) hasEntry(hash string) bool { h.entriesLock.Lock() defer h.entriesLock.Unlock() - for hash, entry := range h.entries { - if entry.exp.Before(time.Now()) { - delete(h.entries, hash) - } - } + h.removeExpiredUnsafe() if _, ok := h.entries[hash]; ok { return true @@ -150,33 +162,46 @@ func (h *sendRecorder) hasEntry(hash string) bool { return false } -// addMessageID should be called after a message has been successfully sent. -func (h *sendRecorder) addMessageID(hash, msgID string) { +// signalMessageSent should be called after a message has been successfully sent. +func (h *sendRecorder) signalMessageSent(hash, msgID string, toList []string) { h.entriesLock.Lock() defer h.entriesLock.Unlock() - entry, ok := h.entries[hash] + entries, ok := h.entries[hash] if ok { - entry.msgID = msgID - } else { - logrus.Warn("Cannot add message ID to send hash entry, it may have expired") + for _, entry := range entries { + if matchToList(entry.toList, toList) { + entry.msgID = msgID + entry.closeWaitChannel() + return + } + } } - entry.closeWaitChannel() + logrus.Warn("Cannot add message ID to send hash entry, it may have expired") } -func (h *sendRecorder) removeOnFail(hash string) { +func (h *sendRecorder) removeOnFail(hash string, toList []string) { h.entriesLock.Lock() defer h.entriesLock.Unlock() - entry, ok := h.entries[hash] - if !ok || entry.msgID != "" { + entries, ok := h.entries[hash] + if !ok { return } - entry.closeWaitChannel() + for idx, entry := range entries { + if entry.msgID == "" && matchToList(entry.toList, toList) { + entry.closeWaitChannel() - delete(h.entries, hash) + remaining := xslices.Remove(entries, idx, 1) + if len(remaining) != 0 { + h.entries[hash] = remaining + } else { + delete(h.entries, hash) + } + } + } } func (h *sendRecorder) wait(ctx context.Context, hash string, deadline time.Time) (string, bool, error) { @@ -200,7 +225,7 @@ func (h *sendRecorder) wait(ctx context.Context, hash string, deadline time.Time defer h.entriesLock.Unlock() if entry, ok := h.entries[hash]; ok { - return entry.msgID, true, nil + return entry[0].msgID, true, nil } return "", false, nil @@ -211,7 +236,7 @@ func (h *sendRecorder) getWaitCh(hash string) (<-chan struct{}, bool) { defer h.entriesLock.Unlock() if entry, ok := h.entries[hash]; ok { - return entry.waitCh, true + return entry[0].waitCh, true } return nil, false diff --git a/internal/user/send_recorder_test.go b/internal/user/send_recorder_test.go index 0aabe90b..942df28e 100644 --- a/internal/user/send_recorder_test.go +++ b/internal/user/send_recorder_test.go @@ -35,7 +35,7 @@ func TestSendHasher_Insert(t *testing.T) { require.NotEmpty(t, hash1) // Simulate successfully sending the message. - h.addMessageID(hash1, "abc") + h.signalMessageSent(hash1, "abc", nil) // Inserting a message with the same hash should return false. _, ok, err = testTryInsert(h, literal1, time.Now().Add(time.Second)) @@ -59,7 +59,7 @@ func TestSendHasher_Insert_Expired(t *testing.T) { require.NotEmpty(t, hash1) // Simulate successfully sending the message. - h.addMessageID(hash1, "abc") + h.signalMessageSent(hash1, "abc", nil) // Wait for the entry to expire. time.Sleep(time.Second) @@ -106,7 +106,7 @@ func TestSendHasher_Wait_SendSuccess(t *testing.T) { // Simulate successfully sending the message after half a second. go func() { time.Sleep(time.Millisecond * 500) - h.addMessageID(hash, "abc") + h.signalMessageSent(hash, "abc", nil) }() // Inserting a message with the same hash should fail. @@ -127,7 +127,7 @@ func TestSendHasher_Wait_SendFail(t *testing.T) { // Simulate failing to send the message after half a second. go func() { time.Sleep(time.Millisecond * 500) - h.removeOnFail(hash) + h.removeOnFail(hash, nil) }() // Inserting a message with the same hash should succeed because the first message failed to send. @@ -163,7 +163,7 @@ func TestSendHasher_HasEntry(t *testing.T) { require.NotEmpty(t, hash) // Simulate successfully sending the message. - h.addMessageID(hash, "abc") + h.signalMessageSent(hash, "abc", nil) // The message was already sent; we should find it in the hasher. messageID, ok, err := testHasEntry(h, literal1, time.Now().Add(time.Second)) @@ -184,7 +184,7 @@ func TestSendHasher_HasEntry_SendSuccess(t *testing.T) { // Simulate successfully sending the message after half a second. go func() { time.Sleep(time.Millisecond * 500) - h.addMessageID(hash, "abc") + h.signalMessageSent(hash, "abc", nil) }() // The message was already sent; we should find it in the hasher. @@ -197,7 +197,7 @@ func TestSendHasher_HasEntry_SendSuccess(t *testing.T) { func TestSendHasher_DualAddDoesNotCauseCrash(t *testing.T) { // There may be a rare case where one 2 smtp connections attempt to send the same message, but if the first message // is stuck long enough for it to expire, the second connection will remove it from the list and cause it to be - // inserted as a new entry. The two clients end up sending the message twice and calling the `addMessageID` x2, + // inserted as a new entry. The two clients end up sending the message twice and calling the `signalMessageSent` x2, // resulting in a crash. h := newSendRecorder(sendEntryExpiry) @@ -209,8 +209,8 @@ func TestSendHasher_DualAddDoesNotCauseCrash(t *testing.T) { // Simulate successfully sending the message. We call this method twice as it possible for multiple SMTP connections // to attempt to send the same message. - h.addMessageID(hash, "abc") - h.addMessageID(hash, "abc") + h.signalMessageSent(hash, "abc", nil) + h.signalMessageSent(hash, "abc", nil) // The message was already sent; we should find it in the hasher. messageID, ok, err := testHasEntry(h, literal1, time.Now().Add(time.Second)) @@ -219,6 +219,22 @@ func TestSendHasher_DualAddDoesNotCauseCrash(t *testing.T) { require.Equal(t, "abc", messageID) } +func TestSendHashed_MessageWithSameHasButDifferentRecipientsIsInserted(t *testing.T) { + h := newSendRecorder(sendEntryExpiry) + + // Insert a message into the hasher. + hash, ok, err := testTryInsert(h, literal1, time.Now().Add(time.Second), "Receiver ") + require.NoError(t, err) + require.True(t, ok) + require.NotEmpty(t, hash) + + hash2, ok, err := testTryInsert(h, literal1, time.Now().Add(time.Second), "Receiver ", "Receiver2 ") + require.NoError(t, err) + require.True(t, ok) + require.NotEmpty(t, hash2) + require.Equal(t, hash, hash2) +} + func TestSendHasher_HasEntry_SendFail(t *testing.T) { h := newSendRecorder(sendEntryExpiry) @@ -231,7 +247,7 @@ func TestSendHasher_HasEntry_SendFail(t *testing.T) { // Simulate failing to send the message after half a second. go func() { time.Sleep(time.Millisecond * 500) - h.removeOnFail(hash) + h.removeOnFail(hash, nil) }() // The message failed to send; we should not find it in the hasher. @@ -265,7 +281,7 @@ func TestSendHasher_HasEntry_Expired(t *testing.T) { require.NotEmpty(t, hash) // Simulate successfully sending the message. - h.addMessageID(hash, "abc") + h.signalMessageSent(hash, "abc", nil) // Wait for the entry to expire. time.Sleep(time.Second) diff --git a/internal/user/smtp.go b/internal/user/smtp.go index 7351c2ca..58148e72 100644 --- a/internal/user/smtp.go +++ b/internal/user/smtp.go @@ -89,7 +89,7 @@ func (user *User) sendMail(authID string, from string, to []string, r io.Reader) } // If we fail to send this message, we should remove the hash from the send recorder. - defer user.sendHash.removeOnFail(hash) + defer user.sendHash.removeOnFail(hash, to) // Create a new message parser from the reader. parser, err := parser.New(bytes.NewReader(b)) @@ -162,7 +162,7 @@ func (user *User) sendMail(authID string, from string, to []string, r io.Reader) } // If the message was successfully sent, we can update the message ID in the record. - user.sendHash.addMessageID(hash, sent.ID) + user.sendHash.signalMessageSent(hash, sent.ID, to) return nil }) From 40dc17aea518bc6ec275ad90f22cf70781c26b5c Mon Sep 17 00:00:00 2001 From: Xavier Michelon Date: Tue, 16 May 2023 16:26:45 +0200 Subject: [PATCH 37/43] feat(GODT-2161): auto-submit 2FA. --- internal/frontend/bridge-gui/bridge-gui/qml/SignIn.qml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/frontend/bridge-gui/bridge-gui/qml/SignIn.qml b/internal/frontend/bridge-gui/bridge-gui/qml/SignIn.qml index d3d9bef8..006eb543 100644 --- a/internal/frontend/bridge-gui/bridge-gui/qml/SignIn.qml +++ b/internal/frontend/bridge-gui/bridge-gui/qml/SignIn.qml @@ -344,7 +344,12 @@ FocusScope { if (str.length === 0) { return qsTr("Enter the 6-digit code") } - return + } + + onTextChanged: { + if (text.length >= 6) { + twoFAButton.onClicked() + } } onAccepted: { From 35f0e081a51ee629f12a2c44dbc11236c18f1d6e Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Wed, 17 May 2023 13:10:28 +0200 Subject: [PATCH 38/43] fix(GODT-2628): Attempt to fix closed channel panic on logout It should not be possible to reach this state on purpose. But due to scheduling and synchronization variances, it is possible in theory that a UserDeathEvent can occur at the same time as the bridge is closing, causing a call to `User.Close()` to be executed 2 times. To avoid this in the future we just clear the map after all the channels have been closed. --- internal/bridge/bridge.go | 2 +- internal/user/user.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/bridge/bridge.go b/internal/bridge/bridge.go index 92b34f33..79453989 100644 --- a/internal/bridge/bridge.go +++ b/internal/bridge/bridge.go @@ -413,7 +413,7 @@ func (bridge *Bridge) Close(ctx context.Context) { } // Close all users. - safe.RLock(func() { + safe.Lock(func() { for _, user := range bridge.users { user.Close() } diff --git a/internal/user/user.go b/internal/user/user.go index 8376fb61..ec010ba3 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -586,6 +586,8 @@ func (user *User) Close() { for _, updateCh := range xslices.Unique(maps.Values(user.updateCh)) { updateCh.CloseAndDiscardQueued() } + + user.updateCh = make(map[string]*async.QueuedChannel[imap.Update]) }, user.updateChLock) // Close the user's notify channel. From bb99695e688c47b1475214ea2506dd7591bdf72b Mon Sep 17 00:00:00 2001 From: Romain Le Jeune Date: Mon, 22 May 2023 09:30:51 +0000 Subject: [PATCH 39/43] feat(GODT-2639): Enhance sentry init log. --- internal/frontend/bridge-gui/bridge-gui/SentryUtils.cpp | 2 +- internal/frontend/bridge-gui/bridge-gui/SentryUtils.h | 1 + internal/frontend/bridge-gui/bridge-gui/main.cpp | 2 ++ internal/sentry/reporter.go | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/frontend/bridge-gui/bridge-gui/SentryUtils.cpp b/internal/frontend/bridge-gui/bridge-gui/SentryUtils.cpp index b7d80720..685b1ffb 100644 --- a/internal/frontend/bridge-gui/bridge-gui/SentryUtils.cpp +++ b/internal/frontend/bridge-gui/bridge-gui/SentryUtils.cpp @@ -49,7 +49,7 @@ QString sentryAttachmentFilePath() { //**************************************************************************************************************************************************** QByteArray getProtectedHostname() { QByteArray hostname = QCryptographicHash::hash(QSysInfo::machineHostName().toUtf8(), QCryptographicHash::Sha256); - return hostname.toHex(); + return hostname.toBase64(); } //**************************************************************************************************************************************************** diff --git a/internal/frontend/bridge-gui/bridge-gui/SentryUtils.h b/internal/frontend/bridge-gui/bridge-gui/SentryUtils.h index 8d1d966e..7e918235 100644 --- a/internal/frontend/bridge-gui/bridge-gui/SentryUtils.h +++ b/internal/frontend/bridge-gui/bridge-gui/SentryUtils.h @@ -22,6 +22,7 @@ #include void initSentry(); +QByteArray getProtectedHostname(); void setSentryReportScope(); sentry_options_t* newSentryOptions(const char * sentryDNS, const char * cacheDir); sentry_uuid_t reportSentryEvent(sentry_level_t level, const char *message); diff --git a/internal/frontend/bridge-gui/bridge-gui/main.cpp b/internal/frontend/bridge-gui/bridge-gui/main.cpp index b7f3225f..617d8b85 100644 --- a/internal/frontend/bridge-gui/bridge-gui/main.cpp +++ b/internal/frontend/bridge-gui/bridge-gui/main.cpp @@ -305,6 +305,8 @@ int main(int argc, char *argv[]) { // When not in attached mode, log entries are forwarded to bridge, which output it on stdout/stderr. bridge-gui's process monitor intercept // these outputs and output them on the command-line. log.setLevel(cliOptions.logLevel); + log.info(QString("New Sentry reporter - id: %1.").arg(getProtectedHostname())); + QString bridgeexec; if (!cliOptions.attach) { if (isBridgeRunning()) { diff --git a/internal/sentry/reporter.go b/internal/sentry/reporter.go index 2b69b4ad..c15832ec 100644 --- a/internal/sentry/reporter.go +++ b/internal/sentry/reporter.go @@ -96,6 +96,7 @@ func GetTimeZone() string { // NewReporter creates new sentry reporter with appName and appVersion to report. func NewReporter(appName string, identifier Identifier) *Reporter { + logrus.WithField("id", GetProtectedHostname()).Info("New sentry reporter") return &Reporter{ appName: appName, appVersion: constants.Revision, From 9c25f56fe6bad746e8169e3a7113821816328cdc Mon Sep 17 00:00:00 2001 From: Romain Le Jeune Date: Mon, 22 May 2023 11:16:56 +0000 Subject: [PATCH 40/43] test: fix flaky tests. --- .gitlab-ci.yml | 2 +- Makefile | 6 +- internal/bridge/bridge_test.go | 37 ++++++++--- internal/bridge/refresh_test.go | 5 +- internal/bridge/send_test.go | 13 ++-- internal/bridge/server_manager_test.go | 16 +++-- internal/bridge/sync_test.go | 8 +-- internal/bridge/user_event_test.go | 63 ++++++++++++------- tests/ctx_imap_test.go | 18 +++++- tests/ctx_test.go | 1 + tests/features/bridge/heartbeat.feature | 4 +- tests/features/imap/auth.feature | 4 +- tests/features/imap/id.feature | 4 +- tests/features/imap/mailbox/create.feature | 4 +- tests/features/imap/mailbox/delete.feature | 4 +- .../imap/mailbox/hide_all_mail.feature | 4 +- tests/features/imap/mailbox/info.feature | 4 +- tests/features/imap/mailbox/list.feature | 4 +- tests/features/imap/mailbox/rename.feature | 4 +- .../imap/mailbox/rename_hiearchy.feature | 4 +- tests/features/imap/mailbox/select.feature | 4 +- tests/features/imap/message/copy.feature | 4 +- tests/features/imap/message/create.feature | 4 +- tests/features/imap/message/delete.feature | 4 +- .../imap/message/delete_from_trash.feature | 1 + tests/features/imap/message/drafts.feature | 8 ++- tests/features/imap/message/fetch.feature | 4 +- tests/features/imap/message/import.feature | 4 +- tests/features/imap/message/move.feature | 4 +- .../imap/message/move_without_support.feature | 4 +- tests/features/imap/migration.feature | 3 +- tests/features/imap/ports.feature | 4 +- tests/features/smtp/auth.feature | 4 +- tests/features/smtp/init.feature | 6 +- tests/features/smtp/ports.feature | 6 +- tests/features/smtp/send/bcc.feature | 8 ++- .../smtp/send/embedded_message.feature | 4 +- tests/features/smtp/send/failures.feature | 4 +- tests/features/smtp/send/html.feature | 4 +- tests/features/smtp/send/html_att.feature | 4 +- tests/features/smtp/send/inline.feature | 4 +- tests/features/smtp/send/mixed_case.feature | 4 +- .../smtp/send/one_account_to_another.feature | 4 +- tests/features/smtp/send/plain.feature | 4 +- tests/features/smtp/send/plain_att.feature | 4 +- tests/features/smtp/send/same_message.feature | 3 +- tests/features/smtp/send/send_append.feature | 4 +- tests/features/smtp/send/send_reply.feature | 4 +- tests/features/smtp/send/two_messages.feature | 4 +- tests/features/user/addressmode.feature | 4 +- tests/features/user/delete.feature | 4 +- tests/features/user/login.feature | 6 +- tests/features/user/relogin.feature | 4 +- tests/features/user/revoke.feature | 4 +- tests/features/user/sync.feature | 4 +- tests/features/user/telemetry.feature | 4 +- tests/user_test.go | 12 +++- 57 files changed, 265 insertions(+), 109 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ddb16c05..ed1ab356 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -99,7 +99,7 @@ test-linux: - .rules-branch-manual-MR-and-devel-always - .after-script-code-coverage tags: - - medium + - large test-linux-race: extends: diff --git a/Makefile b/Makefile index 500e71e5..7d021279 100644 --- a/Makefile +++ b/Makefile @@ -228,13 +228,13 @@ change-copyright-year: ./utils/missing_license.sh change-year test: gofiles - go test -v -timeout=10m -p=1 -count=1 -coverprofile=/tmp/coverage.out -run=${TESTRUN} ./internal/... ./pkg/... + go test -v -timeout=20m -p=1 -count=1 -coverprofile=/tmp/coverage.out -run=${TESTRUN} ./internal/... ./pkg/... test-race: gofiles - go test -v -timeout=30m -p=1 -count=1 -race -failfast -run=${TESTRUN} ./internal/... ./pkg/... + go test -v -timeout=40m -p=1 -count=1 -race -failfast -run=${TESTRUN} ./internal/... ./pkg/... test-integration: gofiles - go test -v -timeout=30m -p=1 -count=1 github.com/ProtonMail/proton-bridge/v3/tests + go test -v -timeout=60m -p=1 -count=1 github.com/ProtonMail/proton-bridge/v3/tests test-integration-debug: gofiles dlv test github.com/ProtonMail/proton-bridge/v3/tests -- -test.v -test.timeout=10m -test.parallel=1 -test.count=1 diff --git a/internal/bridge/bridge_test.go b/internal/bridge/bridge_test.go index c9529e10..676eb612 100644 --- a/internal/bridge/bridge_test.go +++ b/internal/bridge/bridge_test.go @@ -50,7 +50,6 @@ import ( "github.com/ProtonMail/proton-bridge/v3/tests" "github.com/bradenaw/juniper/xslices" imapid "github.com/emersion/go-imap-id" - "github.com/emersion/go-imap/client" "github.com/stretchr/testify/require" ) @@ -182,14 +181,18 @@ func TestBridge_UserAgent_Persistence(t *testing.T) { imapWaiter := waitForIMAPServerReady(b) defer imapWaiter.Done() + smtpWaiter := waitForSMTPServerReady(b) + defer smtpWaiter.Done() + require.NoError(t, getErr(b.LoginFull(ctx, otherUser, otherPassword, nil, nil))) imapWaiter.Wait() + smtpWaiter.Wait() currentUserAgent := b.GetCurrentUserAgent() require.Contains(t, currentUserAgent, vault.DefaultUserAgent) - imapClient, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) + imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) require.NoError(t, err) defer func() { _ = imapClient.Logout() }() @@ -241,11 +244,15 @@ func TestBridge_UserAgentFromIMAPID(t *testing.T) { imapWaiter := waitForIMAPServerReady(b) defer imapWaiter.Done() + smtpWaiter := waitForSMTPServerReady(b) + defer smtpWaiter.Done() + require.NoError(t, getErr(b.LoginFull(ctx, otherUser, otherPassword, nil, nil))) imapWaiter.Wait() + smtpWaiter.Wait() - imapClient, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) + imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) require.NoError(t, err) defer func() { _ = imapClient.Logout() }() @@ -619,6 +626,9 @@ func TestBridge_LoginFailed(t *testing.T) { imapWaiter := waitForIMAPServerReady(bridge) defer imapWaiter.Done() + smtpWaiter := waitForSMTPServerReady(bridge) + defer smtpWaiter.Done() + failCh, done := chToType[events.Event, events.IMAPLoginFailed](bridge.GetEvents(events.IMAPLoginFailed{})) defer done() @@ -626,8 +636,9 @@ func TestBridge_LoginFailed(t *testing.T) { require.NoError(t, err) imapWaiter.Wait() + smtpWaiter.Wait() - imapClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort()))) + imapClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort()))) require.NoError(t, err) require.Error(t, imapClient.Login("badUser", "badPass")) @@ -657,6 +668,9 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) { imapWaiter := waitForIMAPServerReady(b) defer imapWaiter.Done() + smtpWaiter := waitForSMTPServerReady(b) + defer smtpWaiter.Done() + // Login the user. syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{})) defer done() @@ -691,8 +705,9 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) { require.True(t, info.State == bridge.Connected) imapWaiter.Wait() + smtpWaiter.Wait() - client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) + client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) require.NoError(t, err) require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) defer func() { _ = client.Logout() }() @@ -732,7 +747,7 @@ func TestBridge_ChangeAddressOrder(t *testing.T) { require.NoError(t, err) require.True(t, info.State == bridge.Connected) - client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) + client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) require.NoError(t, err) require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) defer func() { _ = client.Logout() }() @@ -753,7 +768,7 @@ func TestBridge_ChangeAddressOrder(t *testing.T) { require.NoError(t, err) require.True(t, info.State == bridge.Connected) - client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) + client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) require.NoError(t, err) require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) defer func() { _ = client.Logout() }() @@ -990,6 +1005,14 @@ func waitForSMTPServerReady(b *bridge.Bridge) *eventWaiter { } } +func waitForSMTPServerStopped(b *bridge.Bridge) *eventWaiter { + evtCh, cancel := b.GetEvents(events.SMTPServerStopped{}) + return &eventWaiter{ + evtCh: evtCh, + cancel: cancel, + } +} + func waitForIMAPServerReady(b *bridge.Bridge) *eventWaiter { evtCh, cancel := b.GetEvents(events.IMAPServerReady{}) return &eventWaiter{ diff --git a/internal/bridge/refresh_test.go b/internal/bridge/refresh_test.go index ad4192f6..e8edcd7d 100644 --- a/internal/bridge/refresh_test.go +++ b/internal/bridge/refresh_test.go @@ -28,7 +28,6 @@ import ( "github.com/ProtonMail/proton-bridge/v3/internal/constants" "github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/bradenaw/juniper/iterator" - "github.com/emersion/go-imap/client" "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" ) @@ -66,7 +65,7 @@ func TestBridge_Refresh(t *testing.T) { require.NoError(t, err) require.True(t, info.State == bridge.Connected) - client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) + client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) require.NoError(t, err) require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) defer func() { _ = client.Logout() }() @@ -99,7 +98,7 @@ func TestBridge_Refresh(t *testing.T) { require.NoError(t, err) require.True(t, info.State == bridge.Connected) - client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) + client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) require.NoError(t, err) require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) defer func() { _ = client.Logout() }() diff --git a/internal/bridge/send_test.go b/internal/bridge/send_test.go index 8015e4ea..a334db15 100644 --- a/internal/bridge/send_test.go +++ b/internal/bridge/send_test.go @@ -34,7 +34,6 @@ import ( "github.com/ProtonMail/proton-bridge/v3/internal/constants" "github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/emersion/go-imap" - "github.com/emersion/go-imap/client" "github.com/emersion/go-sasl" "github.com/emersion/go-smtp" "github.com/stretchr/testify/require" @@ -96,13 +95,13 @@ func TestBridge_Send(t *testing.T) { } // Connect the sender IMAP client. - senderIMAPClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort()))) + senderIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort()))) require.NoError(t, err) require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass))) defer senderIMAPClient.Logout() //nolint:errcheck // Connect the recipient IMAP client. - recipientIMAPClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort()))) + recipientIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort()))) require.NoError(t, err) require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass))) defer recipientIMAPClient.Logout() //nolint:errcheck @@ -146,7 +145,7 @@ func TestBridge_SendDraftFlags(t *testing.T) { require.NoError(t, err) // Connect the sender IMAP client. - imapClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort()))) + imapClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort()))) require.NoError(t, err) require.NoError(t, imapClient.Login(userInfo.Addresses[0], string(userInfo.BridgePass))) defer imapClient.Logout() //nolint:errcheck @@ -256,7 +255,7 @@ func TestBridge_SendInvite(t *testing.T) { require.NoError(t, err) // Connect the sender IMAP client. - imapClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort()))) + imapClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort()))) require.NoError(t, err) require.NoError(t, imapClient.Login(userInfo.Addresses[0], string(userInfo.BridgePass))) defer imapClient.Logout() //nolint:errcheck @@ -454,13 +453,13 @@ SGVsbG8gd29ybGQK } // Connect the sender IMAP client. - senderIMAPClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort()))) + senderIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort()))) require.NoError(t, err) require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass))) defer senderIMAPClient.Logout() //nolint:errcheck // Connect the recipient IMAP client. - recipientIMAPClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort()))) + recipientIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort()))) require.NoError(t, err) require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass))) defer recipientIMAPClient.Logout() //nolint:errcheck diff --git a/internal/bridge/server_manager_test.go b/internal/bridge/server_manager_test.go index 23a94a1d..47f799e9 100644 --- a/internal/bridge/server_manager_test.go +++ b/internal/bridge/server_manager_test.go @@ -27,14 +27,13 @@ import ( "github.com/ProtonMail/proton-bridge/v3/internal/bridge" "github.com/ProtonMail/proton-bridge/v3/internal/constants" "github.com/ProtonMail/proton-bridge/v3/internal/events" - "github.com/emersion/go-imap/client" "github.com/stretchr/testify/require" ) func TestServerManager_NoLoadedUsersNoServers(t *testing.T) { withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { - _, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort())) + _, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort())) require.Error(t, err) }) }) @@ -113,7 +112,7 @@ func TestServerManager_ServersDoNotStopWhenThereIsStillOneActiveUser(t *testing. waitForEvent(t, evtCh, events.UserDeauth{}) - imapClient, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort())) + imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort())) require.NoError(t, err) require.NoError(t, imapClient.Logout()) }) @@ -138,7 +137,7 @@ func TestServerManager_ServersStartIfAtLeastOneUserIsLoggedIn(t *testing.T) { require.NoError(t, s.RevokeUser(userIDOther)) withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { - imapClient, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort())) + imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort())) require.NoError(t, err) require.NoError(t, imapClient.Logout()) }) @@ -151,21 +150,30 @@ func TestServerManager_NetworkLossStopsServers(t *testing.T) { imapWaiter := waitForIMAPServerReady(bridge) defer imapWaiter.Done() + smtpWaiter := waitForSMTPServerReady(bridge) + defer smtpWaiter.Done() + imapWaiterStop := waitForIMAPServerStopped(bridge) defer imapWaiterStop.Done() + smtpWaiterStop := waitForSMTPServerStopped(bridge) + defer smtpWaiterStop.Done() + _, err := bridge.LoginFull(ctx, username, password, nil, nil) require.NoError(t, err) imapWaiter.Wait() + smtpWaiter.Wait() netCtl.Disable() imapWaiterStop.Wait() + smtpWaiterStop.Wait() netCtl.Enable() imapWaiter.Wait() + smtpWaiter.Wait() }) }) } diff --git a/internal/bridge/sync_test.go b/internal/bridge/sync_test.go index c880f7b3..00a0105e 100644 --- a/internal/bridge/sync_test.go +++ b/internal/bridge/sync_test.go @@ -80,7 +80,7 @@ func TestBridge_Sync(t *testing.T) { require.NoError(t, err) require.True(t, info.State == bridge.Connected) - client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) + client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) require.NoError(t, err) require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) defer func() { _ = client.Logout() }() @@ -127,7 +127,7 @@ func TestBridge_Sync(t *testing.T) { require.NoError(t, err) require.True(t, info.State == bridge.Connected) - client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) + client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) require.NoError(t, err) require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) defer func() { _ = client.Logout() }() @@ -178,7 +178,7 @@ func _TestBridge_Sync_BadMessage(t *testing.T) { //nolint:unused,deadcode require.NoError(t, err) require.True(t, info.State == bridge.Connected) - client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) + client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) require.NoError(t, err) require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) defer func() { _ = client.Logout() }() @@ -293,7 +293,7 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) { require.NoError(t, err) require.True(t, info.State == bridge.Connected) - client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) + client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) require.NoError(t, err) require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) defer func() { _ = client.Logout() }() diff --git a/internal/bridge/user_event_test.go b/internal/bridge/user_event_test.go index caae0134..e31ae675 100644 --- a/internal/bridge/user_event_test.go +++ b/internal/bridge/user_event_test.go @@ -444,14 +444,14 @@ func TestBridge_User_DropConn_NoBadEvent(t *testing.T) { info, err := bridge.QueryUserInfo("user") require.NoError(t, err) - client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort())) + cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort())) require.NoError(t, err) - require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) - defer func() { _ = client.Logout() }() + require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass))) + defer func() { _ = cli.Logout() }() // The IMAP client will eventually see 20 messages. require.Eventually(t, func() bool { - status, err := client.Status("INBOX", []imap.StatusItem{imap.StatusMessages}) + status, err := cli.Status("INBOX", []imap.StatusItem{imap.StatusMessages}) return err == nil && status.Messages == 20 }, 10*time.Second, 100*time.Millisecond) }) @@ -645,12 +645,12 @@ func TestBridge_User_SendDraftRemoveDraftFlag(t *testing.T) { info, err := bridge.QueryUserInfo("user") require.NoError(t, err) - client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort())) + cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort())) require.NoError(t, err) - require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) - defer func() { _ = client.Logout() }() + require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass))) + defer func() { _ = cli.Logout() }() - messages, err := clientFetch(client, "Drafts") + messages, err := clientFetch(cli, "Drafts") require.NoError(t, err) require.Len(t, messages, 1) require.Contains(t, messages[0].Flags, imap.DraftFlag) @@ -684,12 +684,12 @@ func TestBridge_User_SendDraftRemoveDraftFlag(t *testing.T) { info, err := bridge.QueryUserInfo("user") require.NoError(t, err) - client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort())) + cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort())) require.NoError(t, err) - require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) - defer func() { _ = client.Logout() }() + require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass))) + defer func() { _ = cli.Logout() }() - messages, err := clientFetch(client, "Sent") + messages, err := clientFetch(cli, "Sent") require.NoError(t, err) require.Len(t, messages, 1) require.NotContains(t, messages[0].Flags, imap.DraftFlag) @@ -781,17 +781,21 @@ func TestBridge_User_HandleParentLabelRename(t *testing.T) { imapWaiter := waitForIMAPServerReady(bridge) defer imapWaiter.Done() + smtpWaiter := waitForSMTPServerReady(bridge) + defer smtpWaiter.Done() + require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil))) info, err := bridge.QueryUserInfo(username) require.NoError(t, err) imapWaiter.Wait() + smtpWaiter.Wait() - client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort())) + cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort())) require.NoError(t, err) - require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) - defer func() { _ = client.Logout() }() + require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass))) + defer func() { _ = cli.Logout() }() withClient(ctx, t, s, username, password, func(ctx context.Context, c *proton.Client) { parentName := uuid.NewString() @@ -807,7 +811,7 @@ func TestBridge_User_HandleParentLabelRename(t *testing.T) { // Wait for the parent folder to be created. require.Eventually(t, func() bool { - return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool { + return xslices.IndexFunc(clientList(cli), func(mailbox *imap.MailboxInfo) bool { return mailbox.Name == fmt.Sprintf("Folders/%v", parentName) }) >= 0 }, 100*user.EventPeriod, user.EventPeriod) @@ -824,7 +828,7 @@ func TestBridge_User_HandleParentLabelRename(t *testing.T) { // Wait for the parent folder to be created. require.Eventually(t, func() bool { - return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool { + return xslices.IndexFunc(clientList(cli), func(mailbox *imap.MailboxInfo) bool { return mailbox.Name == fmt.Sprintf("Folders/%v/%v", parentName, childName) }) >= 0 }, 100*user.EventPeriod, user.EventPeriod) @@ -839,14 +843,14 @@ func TestBridge_User_HandleParentLabelRename(t *testing.T) { // Wait for the parent folder to be renamed. require.Eventually(t, func() bool { - return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool { + return xslices.IndexFunc(clientList(cli), func(mailbox *imap.MailboxInfo) bool { return mailbox.Name == fmt.Sprintf("Folders/%v", newParentName) }) >= 0 }, 100*user.EventPeriod, user.EventPeriod) // Wait for the child folder to be renamed. require.Eventually(t, func() bool { - return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool { + return xslices.IndexFunc(clientList(cli), func(mailbox *imap.MailboxInfo) bool { return mailbox.Name == fmt.Sprintf("Folders/%v/%v", newParentName, childName) }) >= 0 }, 100*user.EventPeriod, user.EventPeriod) @@ -898,10 +902,10 @@ func userContinueEventProcess( info, err := bridge.QueryUserInfo("user") require.NoError(t, err) - client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort())) + cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort())) require.NoError(t, err) - require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) - defer func() { _ = client.Logout() }() + require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass))) + defer func() { _ = cli.Logout() }() randomLabel := uuid.NewString() @@ -916,8 +920,21 @@ func userContinueEventProcess( // Wait for the label to be created. require.Eventually(t, func() bool { - return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool { + return xslices.IndexFunc(clientList(cli), func(mailbox *imap.MailboxInfo) bool { return mailbox.Name == "Labels/"+randomLabel }) >= 0 }, 100*user.EventPeriod, user.EventPeriod) } + +func eventuallyDial(addr string) (cli *client.Client, err error) { + var sleep = 1 * time.Second + for i := 0; i < 5; i++ { + cli, err := client.Dial(addr) + if err == nil { + return cli, nil + } + time.Sleep(sleep) + sleep *= 2 + } + return nil, fmt.Errorf("after 5 attempts, last error: %s", err) +} diff --git a/tests/ctx_imap_test.go b/tests/ctx_imap_test.go index 7f8b3d6a..13b590ad 100644 --- a/tests/ctx_imap_test.go +++ b/tests/ctx_imap_test.go @@ -19,6 +19,7 @@ package tests import ( "fmt" + "time" "github.com/ProtonMail/proton-bridge/v3/internal/constants" "github.com/emersion/go-imap/client" @@ -29,14 +30,14 @@ func (t *testCtx) newIMAPClient(userID, clientID string) error { } func (t *testCtx) newIMAPClientOnPort(userID, clientID string, imapPort int) error { - client, err := client.Dial(fmt.Sprintf("%v:%d", constants.Host, imapPort)) + cli, err := eventuallyDial(fmt.Sprintf("%v:%d", constants.Host, imapPort)) if err != nil { return err } t.imapClients[clientID] = &imapClient{ userID: userID, - client: client, + client: cli, } return nil @@ -45,3 +46,16 @@ func (t *testCtx) newIMAPClientOnPort(userID, clientID string, imapPort int) err func (t *testCtx) getIMAPClient(clientID string) (string, *client.Client) { return t.imapClients[clientID].userID, t.imapClients[clientID].client } + +func eventuallyDial(addr string) (cli *client.Client, err error) { + var sleep = 1 * time.Second + for i := 0; i < 5; i++ { + cli, err := client.Dial(addr) + if err == nil { + return cli, nil + } + time.Sleep(sleep) + sleep *= 2 + } + return nil, fmt.Errorf("after 5 attempts, last error: %s", err) +} diff --git a/tests/ctx_test.go b/tests/ctx_test.go index b37a0a99..f667c5f1 100644 --- a/tests/ctx_test.go +++ b/tests/ctx_test.go @@ -169,6 +169,7 @@ type testCtx struct { dummyListeners []net.Listener imapServerStarted bool + smtpServerStarted bool } type imapClient struct { diff --git a/tests/features/bridge/heartbeat.feature b/tests/features/bridge/heartbeat.feature index 9584f800..1288b7c5 100644 --- a/tests/features/bridge/heartbeat.feature +++ b/tests/features/bridge/heartbeat.feature @@ -1,7 +1,9 @@ Feature: Send Telemetry Heartbeat Background: Given there exists an account with username "[user:user1]" and password "password" - And bridge starts + Then it succeeds + When bridge starts + Then it succeeds Scenario: Send at first start - one user default settings diff --git a/tests/features/imap/auth.feature b/tests/features/imap/auth.feature index d839a3bc..c51539f4 100644 --- a/tests/features/imap/auth.feature +++ b/tests/features/imap/auth.feature @@ -4,9 +4,11 @@ Feature: A user can authenticate an IMAP client And there exists an account with username "[user:user2]" and password "password2" And the account "[user:user]" has additional address "[alias:alias]@[domain]" And the account "[user:user2]" has additional disabled address "[alias:alias2]@[domain]" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And the user logs in with username "[user:user2]" and password "password2" + Then it succeeds Scenario: IMAP client can authenticate successfully When user "[user:user]" connects IMAP client "1" diff --git a/tests/features/imap/id.feature b/tests/features/imap/id.feature index a8653b9e..95d1ab83 100644 --- a/tests/features/imap/id.feature +++ b/tests/features/imap/id.feature @@ -1,8 +1,10 @@ Feature: The IMAP ID is propagated to bridge Background: Given there exists an account with username "[user:user]" and password "password" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" + Then it succeeds Scenario: Initial user agent before an IMAP client announces its ID When user "[user:user]" connects IMAP client "1" diff --git a/tests/features/imap/mailbox/create.feature b/tests/features/imap/mailbox/create.feature index e89ffc7c..c7617b43 100644 --- a/tests/features/imap/mailbox/create.feature +++ b/tests/features/imap/mailbox/create.feature @@ -7,10 +7,12 @@ Feature: IMAP create mailbox | f2 | folder | | l1 | label | | l2 | label | - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" finishes syncing And user "[user:user]" connects and authenticates IMAP client "1" + Then it succeeds Scenario: Create folder When IMAP client "1" creates "Folders/mbox" diff --git a/tests/features/imap/mailbox/delete.feature b/tests/features/imap/mailbox/delete.feature index 01c664ae..0f269dda 100644 --- a/tests/features/imap/mailbox/delete.feature +++ b/tests/features/imap/mailbox/delete.feature @@ -6,10 +6,12 @@ Feature: IMAP delete mailbox | one | folder | | two | folder | | three | label | - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" finishes syncing And user "[user:user]" connects and authenticates IMAP client "1" + Then it succeeds Scenario: Delete folder When IMAP client "1" deletes "Folders/one" diff --git a/tests/features/imap/mailbox/hide_all_mail.feature b/tests/features/imap/mailbox/hide_all_mail.feature index 9a450e8f..4009a1d9 100644 --- a/tests/features/imap/mailbox/hide_all_mail.feature +++ b/tests/features/imap/mailbox/hide_all_mail.feature @@ -1,10 +1,12 @@ Feature: IMAP Hide All Mail Background: Given there exists an account with username "[user:user]" and password "password" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" finishes syncing And user "[user:user]" connects and authenticates IMAP client "1" + Then it succeeds Scenario: Hide All Mail Mailbox Given IMAP client "1" eventually sees the following mailbox info: diff --git a/tests/features/imap/mailbox/info.feature b/tests/features/imap/mailbox/info.feature index 670f0936..bcf938a2 100644 --- a/tests/features/imap/mailbox/info.feature +++ b/tests/features/imap/mailbox/info.feature @@ -8,9 +8,11 @@ Feature: IMAP get mailbox info | from | to | subject | unread | | a@example.com | a@example.com | one | true | | b@example.com | b@example.com | two | false | - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" finishes syncing + Then it succeeds Scenario: Mailbox status reports correct name, total and unread When user "[user:user]" connects and authenticates IMAP client "1" diff --git a/tests/features/imap/mailbox/list.feature b/tests/features/imap/mailbox/list.feature index ac939363..d800019b 100644 --- a/tests/features/imap/mailbox/list.feature +++ b/tests/features/imap/mailbox/list.feature @@ -5,11 +5,13 @@ Feature: IMAP list mailboxes | name | type | | mbox1 | folder | | mbox2 | label | + Then it succeeds When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" finishes syncing And user "[user:user]" connects and authenticates IMAP client "1" - Then IMAP client "1" eventually sees the following mailbox info: + Then it succeeds + And IMAP client "1" eventually sees the following mailbox info: | name | | INBOX | | Drafts | diff --git a/tests/features/imap/mailbox/rename.feature b/tests/features/imap/mailbox/rename.feature index 5e88af5c..25f2bb04 100644 --- a/tests/features/imap/mailbox/rename.feature +++ b/tests/features/imap/mailbox/rename.feature @@ -5,10 +5,12 @@ Feature: IMAP get mailbox info | name | type | | f1 | folder | | l1 | label | - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" finishes syncing And user "[user:user]" connects and authenticates IMAP client "1" + Then it succeeds Scenario: Rename folder When IMAP client "1" renames "Folders/f1" to "Folders/f2" diff --git a/tests/features/imap/mailbox/rename_hiearchy.feature b/tests/features/imap/mailbox/rename_hiearchy.feature index 7bb0de8d..4ca53036 100644 --- a/tests/features/imap/mailbox/rename_hiearchy.feature +++ b/tests/features/imap/mailbox/rename_hiearchy.feature @@ -5,10 +5,12 @@ Feature: IMAP get mailbox info | name | type | | f1 | folder | | f1/f2| folder | - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" finishes syncing And user "[user:user]" connects and authenticates IMAP client "1" + Then it succeeds Scenario: Rename folder with subfolders When IMAP client "1" renames "Folders/f1" to "Folders/f3" diff --git a/tests/features/imap/mailbox/select.feature b/tests/features/imap/mailbox/select.feature index abfa1d9c..af3dfb3d 100644 --- a/tests/features/imap/mailbox/select.feature +++ b/tests/features/imap/mailbox/select.feature @@ -5,10 +5,12 @@ Feature: IMAP select mailbox | name | type | | mbox | folder | | label | label | - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" finishes syncing And user "[user:user]" connects and authenticates IMAP client "1" + Then it succeeds Scenario: Select inbox When IMAP client "1" selects "INBOX" diff --git a/tests/features/imap/message/copy.feature b/tests/features/imap/message/copy.feature index 016fa323..4dd0b6c6 100644 --- a/tests/features/imap/message/copy.feature +++ b/tests/features/imap/message/copy.feature @@ -9,10 +9,12 @@ Feature: IMAP copy messages | from | to | subject | unread | | john.doe@mail.com | [user:user]@[domain] | foo | false | | jane.doe@mail.com | name@[domain] | bar | true | - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" finishes syncing And user "[user:user]" connects and authenticates IMAP client "1" + Then it succeeds Scenario: Copy message to label When IMAP client "1" copies the message with subject "foo" from "INBOX" to "Labels/label" diff --git a/tests/features/imap/message/create.feature b/tests/features/imap/message/create.feature index 367197da..9b7305c4 100644 --- a/tests/features/imap/message/create.feature +++ b/tests/features/imap/message/create.feature @@ -2,10 +2,12 @@ Feature: IMAP create messages Background: Given there exists an account with username "[user:user]" and password "password" And the account "[user:user]" has additional address "[alias:alias]@[domain]" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" finishes syncing And user "[user:user]" connects and authenticates IMAP client "1" + Then it succeeds Scenario: Creates message to user's primary address When IMAP client "1" appends the following messages to "INBOX": diff --git a/tests/features/imap/message/delete.feature b/tests/features/imap/message/delete.feature index 72fcb288..b9247f19 100644 --- a/tests/features/imap/message/delete.feature +++ b/tests/features/imap/message/delete.feature @@ -7,10 +7,12 @@ Feature: IMAP remove messages from mailbox | label | label | And the address "[user:user]@[domain]" of account "[user:user]" has 10 messages in "Folders/mbox" And the address "[user:user]@[domain]" of account "[user:user]" has 1 messages in "Scheduled" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" finishes syncing And user "[user:user]" connects and authenticates IMAP client "1" + Then it succeeds Scenario: Mark message as deleted and EXPUNGE When IMAP client "1" selects "Folders/mbox" diff --git a/tests/features/imap/message/delete_from_trash.feature b/tests/features/imap/message/delete_from_trash.feature index fdc5105e..15446eed 100644 --- a/tests/features/imap/message/delete_from_trash.feature +++ b/tests/features/imap/message/delete_from_trash.feature @@ -5,6 +5,7 @@ Feature: IMAP remove messages from Trash | name | type | | mbox | folder | | label | label | + Then it succeeds Scenario Outline: Message in Trash and some other label is not permanently deleted Given the address "[user:user]@[domain]" of account "[user:user]" has the following messages in "Trash": diff --git a/tests/features/imap/message/drafts.feature b/tests/features/imap/message/drafts.feature index 2c8e6ae7..0d5f9729 100644 --- a/tests/features/imap/message/drafts.feature +++ b/tests/features/imap/message/drafts.feature @@ -1,7 +1,8 @@ Feature: IMAP Draft messages Background: Given there exists an account with username "[user:user]" and password "password" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" finishes syncing And user "[user:user]" connects and authenticates IMAP client "1" @@ -11,12 +12,13 @@ Feature: IMAP Draft messages This is a dra """ - And it succeeds - Then IMAP client "1" eventually sees the following messages in "Drafts": + Then it succeeds + And IMAP client "1" eventually sees the following messages in "Drafts": | body | | This is a dra | And IMAP client "1" eventually sees 1 messages in "Drafts" + Scenario: Draft edited locally When IMAP client "1" marks message 1 as deleted And IMAP client "1" expunges diff --git a/tests/features/imap/message/fetch.feature b/tests/features/imap/message/fetch.feature index f72461b4..f1375c91 100644 --- a/tests/features/imap/message/fetch.feature +++ b/tests/features/imap/message/fetch.feature @@ -7,10 +7,12 @@ Feature: IMAP Fetch And the address "[user:user]@[domain]" of account "[user:user]" has the following messages in "Inbox": | from | to | subject | date | | john.doe@mail.com | [user:user]@[domain] | foo | 13 Jul 69 00:00 +0000 | - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" finishes syncing And user "[user:user]" connects and authenticates IMAP client "1" + Then it succeeds Scenario: Fetch very old message Given IMAP client "1" eventually sees the following messages in "INBOX": diff --git a/tests/features/imap/message/import.feature b/tests/features/imap/message/import.feature index b70ad4c7..3608d133 100644 --- a/tests/features/imap/message/import.feature +++ b/tests/features/imap/message/import.feature @@ -1,10 +1,12 @@ Feature: IMAP import messages Background: Given there exists an account with username "[user:user]" and password "password" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" finishes syncing And user "[user:user]" connects and authenticates IMAP client "1" + Then it succeeds Scenario: Basic message import When IMAP client "1" appends the following message to "INBOX": diff --git a/tests/features/imap/message/move.feature b/tests/features/imap/message/move.feature index 33235de5..1434930f 100644 --- a/tests/features/imap/message/move.feature +++ b/tests/features/imap/message/move.feature @@ -19,10 +19,12 @@ Feature: IMAP move messages And the address "[user:user]@[domain]" of account "[user:user]" has the following messages in "Scheduled": | from | to | subject | unread | | john.doe@mail.com | [user:user]@[domain] | sch | false | - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" finishes syncing And user "[user:user]" connects and authenticates IMAP client "1" + Then it succeeds Scenario: Move message from folder to label (keeps in folder) When IMAP client "1" moves the message with subject "foo" from "INBOX" to "Labels/label" diff --git a/tests/features/imap/message/move_without_support.feature b/tests/features/imap/message/move_without_support.feature index ca81095e..0ef82762 100644 --- a/tests/features/imap/message/move_without_support.feature +++ b/tests/features/imap/message/move_without_support.feature @@ -4,11 +4,13 @@ Feature: IMAP move messages by append and delete (without MOVE support, e.g., Ou And the account "[user:user]" has the following custom mailboxes: | name | type | | mbox | folder | - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" finishes syncing And user "[user:user]" connects and authenticates IMAP client "source" And user "[user:user]" connects and authenticates IMAP client "target" + Then it succeeds Scenario Outline: Move message from to by When IMAP client "source" appends the following message to "": diff --git a/tests/features/imap/migration.feature b/tests/features/imap/migration.feature index 4d6a11a9..485e966f 100644 --- a/tests/features/imap/migration.feature +++ b/tests/features/imap/migration.feature @@ -7,10 +7,11 @@ Feature: Bridge can fully sync an account | jane.doe@mail.com | name@[domain] | bar | true | And the account "[user:user]" has 20 custom folders And the account "[user:user]" has 60 custom labels + Then it succeeds When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" finishes syncing - When user "[user:user]" connects and authenticates IMAP client "1" + And user "[user:user]" connects and authenticates IMAP client "1" Then IMAP client "1" counts 20 mailboxes under "Folders" And IMAP client "1" counts 60 mailboxes under "Labels" diff --git a/tests/features/imap/ports.feature b/tests/features/imap/ports.feature index 11ff9e12..8a221cdc 100644 --- a/tests/features/imap/ports.feature +++ b/tests/features/imap/ports.feature @@ -1,9 +1,11 @@ Feature: A user can connect an IMAP client to custom ports Background: Given there exists an account with username "[user:user]" and password "password" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And the user changes the IMAP port to 1144 + Then it succeeds Scenario: Authenticates successfully on custom port When user "[user:user]" connects IMAP client "1" on port 1144 diff --git a/tests/features/smtp/auth.feature b/tests/features/smtp/auth.feature index 540b291f..067b9321 100644 --- a/tests/features/smtp/auth.feature +++ b/tests/features/smtp/auth.feature @@ -6,10 +6,12 @@ Feature: A user can authenticate an SMTP client And the account "[user:user]" has additional address "[alias:alias]@[domain]" And the account "[user:user2]" has additional disabled address "[alias:alias2]@[domain]" And the account "[user:user3]" has additional address "[alias:alias3]@[domain]" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And the user logs in with username "[user:user2]" and password "password2" And the user logs in with username "[user:user3]" and password "password3" + Then it succeeds Scenario: SMTP client can authenticate successfully When user "[user:user]" connects SMTP client "1" diff --git a/tests/features/smtp/init.feature b/tests/features/smtp/init.feature index 20fb9258..4d5ba114 100644 --- a/tests/features/smtp/init.feature +++ b/tests/features/smtp/init.feature @@ -1,9 +1,11 @@ Feature: SMTP initiation Background: Given there exists an account with username "[user:user]" and password "password" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" - When user "[user:user]" connects and authenticates SMTP client "1" + And user "[user:user]" connects and authenticates SMTP client "1" + Then it succeeds Scenario: Send without first announcing FROM and TO When SMTP client "1" sends DATA: diff --git a/tests/features/smtp/ports.feature b/tests/features/smtp/ports.feature index 82fd17ec..d117d968 100644 --- a/tests/features/smtp/ports.feature +++ b/tests/features/smtp/ports.feature @@ -1,10 +1,12 @@ Feature: A user can connect an SMTP client to custom ports Background: Given there exists an account with username "[user:user]" and password "password" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" - And the user changes the SMTP port to 1144 + Then it succeeds Scenario: Authenticates successfully on custom port + When the user changes the SMTP port to 1144 When user "[user:user]" connects SMTP client "1" on port 1144 Then SMTP client "1" can authenticate \ No newline at end of file diff --git a/tests/features/smtp/send/bcc.feature b/tests/features/smtp/send/bcc.feature index a41f60da..8ae2a770 100644 --- a/tests/features/smtp/send/bcc.feature +++ b/tests/features/smtp/send/bcc.feature @@ -1,12 +1,14 @@ Feature: SMTP with bcc Background: Given there exists an account with username "[user:user]" and password "password" - Given there exists an account with username "[user:to]" and password "password" - Given there exists an account with username "[user:bcc]" and password "password" - And bridge starts + And there exists an account with username "[user:to]" and password "password" + And there exists an account with username "[user:bcc]" and password "password" + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And the user logs in with username "[user:bcc]" and password "password" And user "[user:user]" connects and authenticates SMTP client "1" + Then it succeeds Scenario: Send message to address in to and bcc When SMTP client "1" sends the following message from "[user:user]@[domain]" to "[user:to]@[domain], [user:bcc]@[domain]": diff --git a/tests/features/smtp/send/embedded_message.feature b/tests/features/smtp/send/embedded_message.feature index 3ab685c9..a7c94db3 100644 --- a/tests/features/smtp/send/embedded_message.feature +++ b/tests/features/smtp/send/embedded_message.feature @@ -2,10 +2,12 @@ Feature: SMTP sending embedded message Background: Given there exists an account with username "[user:user]" and password "password" And there exists an account with username "[user:to]" and password "password" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And the user logs in with username "[user:to]" and password "password" And user "[user:user]" connects and authenticates SMTP client "1" + Then it succeeds @long-black Scenario: Send it diff --git a/tests/features/smtp/send/failures.feature b/tests/features/smtp/send/failures.feature index cd93f6a6..19c53538 100644 --- a/tests/features/smtp/send/failures.feature +++ b/tests/features/smtp/send/failures.feature @@ -2,9 +2,11 @@ Feature: SMTP wrong messages Background: Given there exists an account with username "[user:user]" and password "password" And there exists an account with username "[user:to]" and password "password" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" connects and authenticates SMTP client "1" + Then it succeeds Scenario: Message with attachment and wrong boundaries When SMTP client "1" sends the following message from "[user:user]@[domain]" to "[user:to]@[domain]": diff --git a/tests/features/smtp/send/html.feature b/tests/features/smtp/send/html.feature index 0b120d93..5f2d28ea 100644 --- a/tests/features/smtp/send/html.feature +++ b/tests/features/smtp/send/html.feature @@ -2,9 +2,11 @@ Feature: SMTP sending of plain messages Background: Given there exists an account with username "[user:user]" and password "password" And there exists an account with username "[user:to]" and password "password" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" connects and authenticates SMTP client "1" + Then it succeeds Scenario: HTML message to external account When SMTP client "1" sends the following message from "[user:user]@[domain]" to "pm.bridge.qa@gmail.com": diff --git a/tests/features/smtp/send/html_att.feature b/tests/features/smtp/send/html_att.feature index effb7989..5d0a547f 100644 --- a/tests/features/smtp/send/html_att.feature +++ b/tests/features/smtp/send/html_att.feature @@ -2,9 +2,11 @@ Feature: SMTP sending of plain messages Background: Given there exists an account with username "[user:user]" and password "password" And there exists an account with username "[user:to]" and password "password" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" connects and authenticates SMTP client "1" + Then it succeeds Scenario: HTML message with attachment to internal account When SMTP client "1" sends the following message from "[user:user]@[domain]" to "[user:to]@[domain]": diff --git a/tests/features/smtp/send/inline.feature b/tests/features/smtp/send/inline.feature index 29d0fd17..97aab9e7 100644 --- a/tests/features/smtp/send/inline.feature +++ b/tests/features/smtp/send/inline.feature @@ -2,9 +2,11 @@ Feature: SMTP messages containing inlines Background: Given there exists an account with username "[user:user]" and password "password" And there exists an account with username "[user:to]" and password "password" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" connects and authenticates SMTP client "1" + Then it succeeds Scenario: A message with inline attachment to internal account When SMTP client "1" sends the following message from "[user:user]@[domain]" to "[user:to]@[domain]": diff --git a/tests/features/smtp/send/mixed_case.feature b/tests/features/smtp/send/mixed_case.feature index dd6b918b..248b14df 100644 --- a/tests/features/smtp/send/mixed_case.feature +++ b/tests/features/smtp/send/mixed_case.feature @@ -2,9 +2,11 @@ Feature: SMTP sending with mixed case address Background: Given there exists an account with username "[user:user]" and password "password" And there exists an account with username "[user:to]" and password "password" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" connects and authenticates SMTP client "1" + Then it succeeds Scenario: Mixed sender case in sender address When SMTP client "1" sends the following message from "[user:user]@[domain]" to "[user:to]@[domain]": diff --git a/tests/features/smtp/send/one_account_to_another.feature b/tests/features/smtp/send/one_account_to_another.feature index 62e20587..82b29568 100644 --- a/tests/features/smtp/send/one_account_to_another.feature +++ b/tests/features/smtp/send/one_account_to_another.feature @@ -2,9 +2,11 @@ Feature: SMTP sending two messages Background: Given there exists an account with username "[user:user]" and password "password" And there exists an account with username "[user:recp]" and password "password" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And the user logs in with username "[user:recp]" and password "password" + Then it succeeds @long-black diff --git a/tests/features/smtp/send/plain.feature b/tests/features/smtp/send/plain.feature index 1dffe325..a8427333 100644 --- a/tests/features/smtp/send/plain.feature +++ b/tests/features/smtp/send/plain.feature @@ -3,9 +3,11 @@ Feature: SMTP sending of plain messages Given there exists an account with username "[user:user]" and password "password" And there exists an account with username "[user:to]" and password "password" And there exists an account with username "[user:cc]" and password "password" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" connects and authenticates SMTP client "1" + Then it succeeds Scenario: Only from and to headers to internal account When SMTP client "1" sends the following message from "[user:user]@[domain]" to "[user:to]@[domain]": diff --git a/tests/features/smtp/send/plain_att.feature b/tests/features/smtp/send/plain_att.feature index 41a9112f..009e6d8c 100644 --- a/tests/features/smtp/send/plain_att.feature +++ b/tests/features/smtp/send/plain_att.feature @@ -2,9 +2,11 @@ Feature: SMTP sending of plain messages Background: Given there exists an account with username "[user:user]" and password "password" And there exists an account with username "[user:to]" and password "password" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" connects and authenticates SMTP client "1" + Then it succeeds Scenario: Basic message with attachment to internal account When SMTP client "1" sends the following message from "[user:user]@[domain]" to "[user:to]@[domain]": diff --git a/tests/features/smtp/send/same_message.feature b/tests/features/smtp/send/same_message.feature index 394b28d3..f877f659 100644 --- a/tests/features/smtp/send/same_message.feature +++ b/tests/features/smtp/send/same_message.feature @@ -2,7 +2,8 @@ Feature: SMTP sending the same message twice Background: Given there exists an account with username "[user:user]" and password "password" And there exists an account with username "[user:to]" and password "password" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And the user logs in with username "[user:to]" and password "password" And user "[user:user]" connects and authenticates SMTP client "1" diff --git a/tests/features/smtp/send/send_append.feature b/tests/features/smtp/send/send_append.feature index 1d475360..57e5b443 100644 --- a/tests/features/smtp/send/send_append.feature +++ b/tests/features/smtp/send/send_append.feature @@ -2,10 +2,12 @@ Feature: SMTP sending with APPENDing to Sent Background: Given there exists an account with username "[user:user]" and password "password" And there exists an account with username "[user:to]" and password "password" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" connects and authenticates SMTP client "1" And user "[user:user]" connects and authenticates IMAP client "1" + Then it succeeds Scenario: Send message and append to Sent # First do sending. diff --git a/tests/features/smtp/send/send_reply.feature b/tests/features/smtp/send/send_reply.feature index 095b5749..4727e1bc 100644 --- a/tests/features/smtp/send/send_reply.feature +++ b/tests/features/smtp/send/send_reply.feature @@ -3,11 +3,13 @@ Feature: SMTP send reply Background: Given there exists an account with username "[user:user1]" and password "password" And there exists an account with username "[user:user2]" and password "password" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user1]" and password "password" And user "[user:user1]" finishes syncing And user "[user:user1]" connects and authenticates SMTP client "1" And user "[user:user1]" connects and authenticates IMAP client "1" + Then it succeeds @long-black Scenario: Reply with In-Reply-To but no References diff --git a/tests/features/smtp/send/two_messages.feature b/tests/features/smtp/send/two_messages.feature index 509c848c..e484a566 100644 --- a/tests/features/smtp/send/two_messages.feature +++ b/tests/features/smtp/send/two_messages.feature @@ -4,10 +4,12 @@ Feature: SMTP sending two messages And there exists an account with username "[user:multi]" and password "password" And the account "[user:multi]" has additional address "[user:multi-alias]@[domain]" And there exists an account with username "[user:to]" and password "password" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And the user logs in with username "[user:multi]" and password "password" And the user sets the address mode of user "[user:multi]" to "split" + Then it succeeds Scenario: Send two messages in one connection When user "[user:user]" connects and authenticates SMTP client "1" diff --git a/tests/features/user/addressmode.feature b/tests/features/user/addressmode.feature index 714e2e3c..026c3205 100644 --- a/tests/features/user/addressmode.feature +++ b/tests/features/user/addressmode.feature @@ -14,9 +14,11 @@ Feature: Address mode | from | to | subject | unread | | c@[domain] | c@[domain] | three | true | | d@[domain] | d@[domain] | four | false | - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" And user "[user:user]" finishes syncing + Then it succeeds Scenario: The user is in combined mode When user "[user:user]" connects and authenticates IMAP client "1" with address "[user:user]@[domain]" diff --git a/tests/features/user/delete.feature b/tests/features/user/delete.feature index e4027df4..2d586a79 100644 --- a/tests/features/user/delete.feature +++ b/tests/features/user/delete.feature @@ -1,8 +1,10 @@ Feature: A user can be deleted Background: Given there exists an account with username "[user:user]" and password "password" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" + Then it succeeds Scenario: Delete a connected user When user "[user:user]" is deleted diff --git a/tests/features/user/login.feature b/tests/features/user/login.feature index 1e512431..e38d74fc 100644 --- a/tests/features/user/login.feature +++ b/tests/features/user/login.feature @@ -1,9 +1,11 @@ Feature: A user can login Background: Given there exists an account with username "[user:user]" and password "password" - Given there exists an account with username "[user:MixedCaps]" and password "password" - Given there exists a disabled account with username "[user:disabled]" and password "password" + And there exists an account with username "[user:MixedCaps]" and password "password" + And there exists a disabled account with username "[user:disabled]" and password "password" + Then it succeeds And bridge starts + Then it succeeds Scenario: Login to account When the user logs in with username "[user:user]" and password "password" diff --git a/tests/features/user/relogin.feature b/tests/features/user/relogin.feature index faf5f5cd..cf1b5a9e 100644 --- a/tests/features/user/relogin.feature +++ b/tests/features/user/relogin.feature @@ -1,8 +1,10 @@ Feature: A logged out user can login again Background: Given there exists an account with username "[user:user]" and password "password" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" + Then it succeeds Scenario: Login to disconnected account When user "[user:user]" logs out diff --git a/tests/features/user/revoke.feature b/tests/features/user/revoke.feature index f0d59055..1eb984ac 100644 --- a/tests/features/user/revoke.feature +++ b/tests/features/user/revoke.feature @@ -1,8 +1,10 @@ Feature: A logged in user is logged out when its auth is revoked. Background: Given there exists an account with username "[user:user]" and password "password" - And bridge starts + Then it succeeds + When bridge starts And the user logs in with username "[user:user]" and password "password" + Then it succeeds Scenario: The auth is revoked while bridge is running When the auth of user "[user:user]" is revoked diff --git a/tests/features/user/sync.feature b/tests/features/user/sync.feature index 260eea3d..a939c1b8 100644 --- a/tests/features/user/sync.feature +++ b/tests/features/user/sync.feature @@ -14,7 +14,9 @@ Feature: Bridge can fully sync an account | from | to | subject | unread | | a@[domain] | a@[domain] | one | true | | b@[domain] | b@[domain] | two | false | - And bridge starts + Then it succeeds + When bridge starts + Then it succeeds Scenario: The account is synced when the user logs in and persists across bridge restarts When the user logs in with username "[user:user]" and password "password" diff --git a/tests/features/user/telemetry.feature b/tests/features/user/telemetry.feature index ff98029b..d1120acc 100644 --- a/tests/features/user/telemetry.feature +++ b/tests/features/user/telemetry.feature @@ -2,7 +2,9 @@ Feature: Bridge send usage metrics Background: Given there exists an account with username "[user:user1]" and password "password" And there exists an account with username "[user:user2]" and password "password" - And bridge starts + Then it succeeds + When bridge starts + Then it succeeds Scenario: Telemetry availability - No user diff --git a/tests/user_test.go b/tests/user_test.go index f1dffda8..a6b31636 100644 --- a/tests/user_test.go +++ b/tests/user_test.go @@ -332,8 +332,10 @@ func (s *scenario) drafAtIndexWasMovedToTrashForAddressOfAccount(draftIndex int, } func (s *scenario) userLogsInWithUsernameAndPassword(username, password string) error { - evtCh, cancel := s.t.bridge.GetEvents(events.SMTPServerReady{}) - defer cancel() + smtpEvtCh, cancelSMTP := s.t.bridge.GetEvents(events.SMTPServerReady{}) + defer cancelSMTP() + imapEvtCh, cancelIMAP := s.t.bridge.GetEvents(events.IMAPServerReady{}) + defer cancelIMAP() userID, err := s.t.bridge.LoginFull(context.Background(), username, []byte(password), nil, nil) if err != nil { @@ -342,9 +344,13 @@ func (s *scenario) userLogsInWithUsernameAndPassword(username, password string) // We need to wait for server to be up or we won't be able to connect. It should only happen once to avoid // blocking on multiple Logins. if !s.t.imapServerStarted { - <-evtCh + <-imapEvtCh s.t.imapServerStarted = true } + if !s.t.smtpServerStarted { + <-smtpEvtCh + s.t.smtpServerStarted = true + } if userID != s.t.getUserByName(username).getUserID() { return errors.New("user ID mismatch") From b6eb5a1b1372cd3e37e0e5c5870de6b3eb1bc16d Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Tue, 23 May 2023 09:32:54 +0200 Subject: [PATCH 41/43] fix(GODT-2454): Only Send status update if transaction succeeded https://github.com/ProtonMail/gluon/pull/354 --- go.mod | 2 +- go.sum | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 4f04d632..5a6e1d7f 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 github.com/Masterminds/semver/v3 v3.2.0 - github.com/ProtonMail/gluon v0.16.1-0.20230516135940-edc685bb7ebb + github.com/ProtonMail/gluon v0.16.1-0.20230522171255-5222638a1050 github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a github.com/ProtonMail/go-proton-api v0.4.1-0.20230516070548-faf4f87bf9e7 github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton diff --git a/go.sum b/go.sum index 48716930..0688e8e1 100644 --- a/go.sum +++ b/go.sum @@ -28,10 +28,8 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo= github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk= github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g= -github.com/ProtonMail/gluon v0.16.1-0.20230516073349-d18e5932b28f h1:z01cLLqPBrS9mHshUgE5444Qpcl0Cpz96ldfqxRTseU= -github.com/ProtonMail/gluon v0.16.1-0.20230516073349-d18e5932b28f/go.mod h1:yA4hk6CJw0BMo+YL8Y3ckCYs5L20sysu9xseshwY3QI= -github.com/ProtonMail/gluon v0.16.1-0.20230516135940-edc685bb7ebb h1:LigybrCpBqujX48/K3y8v5pdtYCDmOwHD+H0liIS0P4= -github.com/ProtonMail/gluon v0.16.1-0.20230516135940-edc685bb7ebb/go.mod h1:yA4hk6CJw0BMo+YL8Y3ckCYs5L20sysu9xseshwY3QI= +github.com/ProtonMail/gluon v0.16.1-0.20230522171255-5222638a1050 h1:U8jsMpk2D101y0kFIuNK5Exp5hgzZTipDE8rr1Jrhmk= +github.com/ProtonMail/gluon v0.16.1-0.20230522171255-5222638a1050/go.mod h1:yA4hk6CJw0BMo+YL8Y3ckCYs5L20sysu9xseshwY3QI= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= From ebe45d5abe4d70525fe3c01d36edc362333923bc Mon Sep 17 00:00:00 2001 From: Romain LE JEUNE Date: Tue, 23 May 2023 11:28:51 +0200 Subject: [PATCH 42/43] fix(GODT-2646): Bump GPA and Gluon dependecy after CIRCL upgrade. --- go.mod | 30 ++++++++++++++--------------- go.sum | 59 +++++++++++++++++++++++++++++++++------------------------- 2 files changed, 49 insertions(+), 40 deletions(-) diff --git a/go.mod b/go.mod index 5a6e1d7f..87cb8bc8 100644 --- a/go.mod +++ b/go.mod @@ -5,14 +5,14 @@ go 1.18 require ( github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 github.com/Masterminds/semver/v3 v3.2.0 - github.com/ProtonMail/gluon v0.16.1-0.20230522171255-5222638a1050 + github.com/ProtonMail/gluon v0.16.1-0.20230523090642-633e61ce9bc2 github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a - github.com/ProtonMail/go-proton-api v0.4.1-0.20230516070548-faf4f87bf9e7 + github.com/ProtonMail/go-proton-api v0.4.1-0.20230523092337-ea8de5f674b7 github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton github.com/PuerkitoBio/goquery v1.8.1 github.com/abiosoft/ishell v2.0.0+incompatible github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 - github.com/bradenaw/juniper v0.10.2 + github.com/bradenaw/juniper v0.12.0 github.com/cucumber/godog v0.12.5 github.com/cucumber/messages-go/v16 v16.0.1 github.com/docker/docker-credential-helpers v0.6.3 @@ -37,15 +37,15 @@ require ( github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 github.com/pkg/errors v0.9.1 github.com/pkg/profile v1.7.0 - github.com/sirupsen/logrus v1.9.0 + github.com/sirupsen/logrus v1.9.2 github.com/stretchr/testify v1.8.1 github.com/urfave/cli/v2 v2.24.4 github.com/vmihailenco/msgpack/v5 v5.3.5 go.uber.org/goleak v1.2.1 - golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb - golang.org/x/net v0.8.0 - golang.org/x/sys v0.6.0 - golang.org/x/text v0.8.0 + golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 + golang.org/x/net v0.10.0 + golang.org/x/sys v0.8.0 + golang.org/x/text v0.9.0 google.golang.org/grpc v1.53.0 google.golang.org/protobuf v1.28.1 howett.net/plist v1.0.0 @@ -55,17 +55,17 @@ require ( ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb // indirect entgo.io/ent v0.11.8 // indirect github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect - github.com/ProtonMail/go-crypto v0.0.0-20230322105811-d73448b7e800 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect - github.com/ProtonMail/go-srp v0.0.5 // indirect + github.com/ProtonMail/go-srp v0.0.7 // indirect github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect github.com/agext/levenshtein v1.2.3 // indirect - github.com/andybalholm/cascadia v1.3.1 // indirect + github.com/andybalholm/cascadia v1.3.2 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/bytedance/sonic v1.8.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/chzyer/test v1.0.0 // indirect - github.com/cloudflare/circl v1.3.2 // indirect + github.com/cloudflare/circl v1.3.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/cronokirby/saferith v0.33.0 // indirect github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect @@ -73,7 +73,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/elastic/go-windows v1.0.1 // indirect github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect - github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a // indirect + github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 // indirect github.com/felixge/fgprof v0.9.3 // indirect github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect github.com/gin-contrib/sse v0.1.0 // indirect @@ -116,9 +116,9 @@ require ( github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/zclconf/go-cty v1.12.1 // indirect golang.org/x/arch v0.2.0 // indirect - golang.org/x/crypto v0.7.0 // indirect + golang.org/x/crypto v0.9.0 // indirect golang.org/x/mod v0.8.0 // indirect - golang.org/x/sync v0.1.0 // indirect + golang.org/x/sync v0.2.0 // indirect golang.org/x/tools v0.6.0 // indirect google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 0688e8e1..198714dd 100644 --- a/go.sum +++ b/go.sum @@ -28,21 +28,22 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo= github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk= github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g= -github.com/ProtonMail/gluon v0.16.1-0.20230522171255-5222638a1050 h1:U8jsMpk2D101y0kFIuNK5Exp5hgzZTipDE8rr1Jrhmk= -github.com/ProtonMail/gluon v0.16.1-0.20230522171255-5222638a1050/go.mod h1:yA4hk6CJw0BMo+YL8Y3ckCYs5L20sysu9xseshwY3QI= +github.com/ProtonMail/gluon v0.16.1-0.20230523090642-633e61ce9bc2 h1:EFmaapQ2BM5OZ16+/c03108+wAt5nq1m/eCzHMl2Vg4= +github.com/ProtonMail/gluon v0.16.1-0.20230523090642-633e61ce9bc2/go.mod h1:ERZikuN+2i/oTeSwS5fq7J0Fms76uUcBlTAwT4KaEAk= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4= -github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= -github.com/ProtonMail/go-crypto v0.0.0-20230322105811-d73448b7e800 h1:o8/VQLSiuRkkSAfVOpFCG1GnTsWxFIOPLvJ2O7hJcFg= +github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE= github.com/ProtonMail/go-crypto v0.0.0-20230322105811-d73448b7e800/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE= +github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 h1:ZK3C5DtzV2nVAQTx5S5jQvMeDqWtD1By5mOoyY/xJek= +github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE= github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753 h1:I8IsYA297x0QLU80G5I6aLYUu3JYNSpo8j5fkXtFDW0= github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= -github.com/ProtonMail/go-proton-api v0.4.1-0.20230516070548-faf4f87bf9e7 h1:7aY4azqc8PzYtg4+xG7b9wBEnckrl7rVMlMoFMWRkdA= -github.com/ProtonMail/go-proton-api v0.4.1-0.20230516070548-faf4f87bf9e7/go.mod h1:UkrG9gN2o9mzdx/an0XRc6a4s5Haef1A7Eyd2iXlw28= -github.com/ProtonMail/go-srp v0.0.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg= -github.com/ProtonMail/go-srp v0.0.5/go.mod h1:06iYHtLXW8vjLtccWj++x3MKy65sIT8yZd7nrJF49rs= +github.com/ProtonMail/go-proton-api v0.4.1-0.20230523092337-ea8de5f674b7 h1:LL+cERFLR5m3AKr6G58AVpsSuQQXulYf9WWWJ+2HUkY= +github.com/ProtonMail/go-proton-api v0.4.1-0.20230523092337-ea8de5f674b7/go.mod h1:e3EhDR9nqGf4sR6OLTBuJ9JmPnB/RLC/U7q0mN11Vmo= +github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI= +github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk= github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton h1:YS6M20yvjCJPR1r4ADW5TPn6rahs4iAyZaACei86bEc= github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton/go.mod h1:S1lYsaGHykYpxxh2SnJL6ypcAlANKj5NRSY6HxKryKQ= github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= @@ -57,8 +58,9 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 h1:28uU3TtuvQ6KRndxg9TrC868jBWmSKgh0GTXkACCXmA= github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37/go.mod h1:6AXRstqK+32jeFmw89QGL2748+dj34Av4xc/I9oo9BY= -github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= +github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -68,8 +70,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/bradenaw/juniper v0.10.2 h1:EY7r8SJJrigJ7lvWk6ews3K5RD4XTG9z+WSwHJKijP4= -github.com/bradenaw/juniper v0.10.2/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI= +github.com/bradenaw/juniper v0.12.0 h1:Q/7icpPQD1nH/La5DobQfNEtwyrBSiSu47jOQx7lJEM= +github.com/bradenaw/juniper v0.12.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.8.1 h1:NqAHCaGaTzro0xMmnTCLUyRlbEP6r8MCA1cJUrH3Pu4= @@ -87,8 +89,8 @@ github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= -github.com/cloudflare/circl v1.3.2 h1:VWp8dY3yH69fdM7lM6A1+NhhVoDu9vqK0jOgmkQHFWk= -github.com/cloudflare/circl v1.3.2/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw= +github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -131,8 +133,8 @@ github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d h1:hFRM6zCBSc+ github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= -github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a h1:cltZpe6s0SJtqK5c/5y2VrIYi8BAtDM6qjmiGYqfTik= -github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= +github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 h1:hQ1wTMaKcGfobYRT88RM8NFNyX+IQHvagkm/tqViU98= +github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -362,8 +364,8 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= +github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= @@ -430,17 +432,17 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w= -golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -480,8 +482,10 @@ golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -492,8 +496,9 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -524,12 +529,15 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -538,8 +546,9 @@ golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhO golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From aa72fd641d51b4369042322628a26a4af7c07a3b Mon Sep 17 00:00:00 2001 From: Jakub Cuth Date: Tue, 23 May 2023 13:37:12 +0000 Subject: [PATCH 43/43] feat(GODT-2631): Bump go to 1.20. --- .gitlab-ci.yml | 14 +++++++------- BUILDS.md | 2 +- go.mod | 2 +- internal/app/app.go | 5 ----- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ed1ab356..7062ae78 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,7 +16,7 @@ # along with ProtonMail Bridge. If not, see . --- -image: harbor.protontech.ch/docker.io/library/golang:1.18 +image: gitlab.protontech.ch:4567/go/bridge-internal:test-go1.20 variables: GOPRIVATE: gitlab.protontech.ch @@ -126,10 +126,10 @@ test-integration-race: .windows-base: before_script: - - export GOROOT=/c/Go1.18 + - export GOROOT=/c/Go1.20 - export PATH=$GOROOT/bin:$PATH - export GOARCH=amd64 - - export GOPATH=~/go18 + - export GOPATH=~/go1.20 - export GO111MODULE=on - export PATH=$GOPATH/bin:$PATH - export MSYSTEM= @@ -172,7 +172,7 @@ test-windows: .linux-build-setup: - image: gitlab.protontech.ch:4567/go/bridge-internal:qt6 + image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.3.2 variables: VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache cache: @@ -209,7 +209,7 @@ build-linux-qa: - export PATH=/usr/local/opt/make/libexec/gnubin:$PATH - export PATH=/usr/local/opt/go@1.13/bin:$PATH - export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH - - export GOPATH=~/go + - export GOPATH=~/go1.20 - export PATH=$GOPATH/bin:$PATH - export CGO_CPPFLAGS='-Wno-error -Wno-nullability-completeness -Wno-expansion-to-defined -Wno-builtin-requires-header' - $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove" @@ -231,10 +231,10 @@ build-darwin-qa: .windows-build-setup: before_script: - - export GOROOT=/c/Go1.18/ + - export GOROOT=/c/Go1.20/ - export PATH=$GOROOT/bin:$PATH - export GOARCH=amd64 - - export GOPATH=~/go18 + - export GOPATH=~/go1.20 - export GO111MODULE=on - export PATH="${GOPATH}/bin:${PATH}" - export MSYSTEM= diff --git a/BUILDS.md b/BUILDS.md index eb4abc7a..237694bd 100644 --- a/BUILDS.md +++ b/BUILDS.md @@ -3,7 +3,7 @@ ## Prerequisites * 64-bit OS: - the go-rfc5322 module cannot currently be compiled for 32-bit OSes -* Go 1.18 +* Go 1.20 * Bash with basic build utils: make, gcc, sed, find, grep, ... - For Windows, it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/) * GCC (Linux), msvc (Windows) or Xcode (macOS) diff --git a/go.mod b/go.mod index 87cb8bc8..d109e84c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ProtonMail/proton-bridge/v3 -go 1.18 +go 1.20 require ( github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 diff --git a/internal/app/app.go b/internal/app/app.go index 16487e51..f2c15178 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -19,14 +19,12 @@ package app import ( "fmt" - "math/rand" "net/http" "net/http/cookiejar" "net/url" "os" "path/filepath" "runtime" - "time" "github.com/Masterminds/semver/v3" "github.com/ProtonMail/gluon/async" @@ -160,9 +158,6 @@ func New() *cli.App { } func run(c *cli.Context) error { - // Seed the default RNG from the math/rand package. - rand.Seed(time.Now().UnixNano()) - // Get the current bridge version. version, err := semver.NewVersion(constants.Version) if err != nil {