diff --git a/build/wino-mail.sqlite b/build/wino-mail.sqlite new file mode 100644 index 0000000..37758a3 Binary files /dev/null and b/build/wino-mail.sqlite differ diff --git a/src/core/authenticator.cpp b/src/core/authenticator.cpp index f27e8ff..9b3419c 100644 --- a/src/core/authenticator.cpp +++ b/src/core/authenticator.cpp @@ -9,33 +9,43 @@ Authenticator::Authenticator(QObject *parent) void Authenticator::exchangeAuthorizationCode(const QString &code, const QString &redirectUri) { - Q_UNUSED(code); - Q_UNUSED(redirectUri); - qDebug() << "[Authenticator] exchangeAuthorizationCode called (base implementation)"; + QUrl url(m_tokenEndpoint); + QNetworkRequest request = createTokenRequest(url); + + QByteArray postData; + postData.append("grant_type=authorization_code"); + postData.append("&code=" + code.toUtf8()); + postData.append("&redirect_uri=" + redirectUri.toUtf8()); + postData.append("&client_id=" + QString(m_clientId).toUtf8()); + postData.append("&client_secret=" + QString(m_clientSecret).toUtf8()); + + QNetworkReply *reply = m_networkManager->post(request, postData); + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + handleTokenResponse(reply->readAll(), m_email, providerName()); + reply->deleteLater(); + }); } void Authenticator::handleTokenResponse(const QByteArray &data, const QString &email, const QString &providerType) { QJsonDocument doc = QJsonDocument::fromJson(data); - if (doc.isNull() || !doc.isObject()) { - emit authenticationFailed("Invalid token response"); + if (!doc.isObject()) { + emit authenticationFailed("Invalid response from server"); return; } QJsonObject obj = doc.object(); - QString accessToken = obj["access_token"].toString(); - QString refreshToken = obj["refresh_token"].toString(); - int expiresIn = obj["expires_in"].toInt(); + if (obj.contains("error")) { + emit authenticationFailed(obj["error"].toString() + ": " + obj["error_description"].toString()); + return; + } Account account; account.setEmail(email); - account.setAccessToken(accessToken); - account.setRefreshToken(refreshToken); - account.setTokenExpires(QDateTime::currentDateTimeUtc().addSecs(expiresIn)); - - if (providerType == "gmail") account.setType(AccountType::Gmail); - else if (providerType == "outlook") account.setType(AccountType::Outlook); - else account.setType(AccountType::IMAP); + account.setProvider(providerType); + account.setAccessToken(obj["access_token"].toString()); + account.setRefreshToken(obj["refresh_token"].toString()); + account.setTokenExpires(QDateTime::currentDateTimeUtc().addSecs(obj["expires_in"].toInt())); emit authenticationCompleted(account); } diff --git a/src/core/gmailauthenticator.cpp b/src/core/gmailauthenticator.cpp index deb5a2d..75cbdc7 100644 --- a/src/core/gmailauthenticator.cpp +++ b/src/core/gmailauthenticator.cpp @@ -24,7 +24,7 @@ GmailAuthenticator::GmailAuthenticator(QObject *parent) connect(m_callbackServer, &OAuthCallbackServer::codeReceived, this, &GmailAuthenticator::onAuthCodeReceived); connect(m_callbackServer, &OAuthCallbackServer::errorOccurred, - this, [this](const QString &error) { + [this](const QString &error) { emit authenticationFailed("OAuth callback error: " + error); }); } @@ -66,19 +66,8 @@ void GmailAuthenticator::onAuthCodeReceived(const QString &code, const QString & m_authCode = code; - // Exchange the authorization code for tokens - QUrlQuery params; - params.addQueryItem("code", code); - params.addQueryItem("client_id", m_clientId); - params.addQueryItem("client_secret", m_clientSecret); - params.addQueryItem("redirect_uri", m_redirectUri); - params.addQueryItem("grant_type", "authorization_code"); - - QNetworkRequest request = createTokenRequest(QUrl(m_tokenEndpoint)); - QNetworkReply *reply = m_networkManager->post(request, params.toString(QUrl::FullyEncoded).toUtf8()); - connect(reply, &QNetworkReply::finished, this, [this, reply]() { - onTokenReply(reply); - }); + // Exchange the authorization code for tokens using base class implementation + exchangeAuthorizationCode(code, m_redirectUri); } void GmailAuthenticator::refreshToken(const Account &account) @@ -94,23 +83,9 @@ void GmailAuthenticator::refreshToken(const Account &account) QNetworkRequest request = createTokenRequest(QUrl(m_tokenEndpoint)); QNetworkReply *reply = m_networkManager->post(request, params.toString(QUrl::FullyEncoded).toUtf8()); connect(reply, &QNetworkReply::finished, this, [this, reply]() { - onTokenReply(reply); + handleTokenResponse(reply->readAll(), m_email, "gmail"); + reply->deleteLater(); }); } -void GmailAuthenticator::onTokenReply(QNetworkReply *reply) -{ - if (reply->error() == QNetworkReply::NoError) { - QByteArray data = reply->readAll(); - handleTokenResponse(data, m_email, "gmail"); - } else { - QByteArray responseData = reply->readAll(); - qWarning() << "[GmailAuthenticator] Token request failed:" - << reply->errorString() - << "Response:" << QString::fromUtf8(responseData); - emit authenticationFailed("Gmail token request failed: " + reply->errorString()); - } - reply->deleteLater(); -} - -#include "gmailauthenticator.moc" \ No newline at end of file +#include "gmailauthenticator.moc" diff --git a/src/core/outlookauthenticator.cpp b/src/core/outlookauthenticator.cpp index 56c9349..5394872 100644 --- a/src/core/outlookauthenticator.cpp +++ b/src/core/outlookauthenticator.cpp @@ -24,7 +24,7 @@ OutlookAuthenticator::OutlookAuthenticator(QObject *parent) connect(m_callbackServer, &OAuthCallbackServer::codeReceived, this, &OutlookAuthenticator::onAuthCodeReceived); connect(m_callbackServer, &OAuthCallbackServer::errorOccurred, - this, [this](const QString &error) { + [this](const QString &error) { emit authenticationFailed("OAuth callback error: " + error); }); } @@ -58,18 +58,7 @@ void OutlookAuthenticator::onAuthCodeReceived(const QString &code, const QString m_callbackServer->stop(); - QUrlQuery params; - params.addQueryItem("code", code); - params.addQueryItem("client_id", m_clientId); - params.addQueryItem("client_secret", m_clientSecret); - params.addQueryItem("redirect_uri", m_redirectUri); - params.addQueryItem("grant_type", "authorization_code"); - - QNetworkRequest request = createTokenRequest(QUrl(m_tokenEndpoint)); - QNetworkReply *reply = m_networkManager->post(request, params.toString(QUrl::FullyEncoded).toUtf8()); - connect(reply, &QNetworkReply::finished, this, [this, reply]() { - onTokenReply(reply); - }); + exchangeAuthorizationCode(code, m_redirectUri); } void OutlookAuthenticator::refreshToken(const Account &account) @@ -86,23 +75,9 @@ void OutlookAuthenticator::refreshToken(const Account &account) QNetworkRequest request = createTokenRequest(QUrl(m_tokenEndpoint)); QNetworkReply *reply = m_networkManager->post(request, params.toString(QUrl::FullyEncoded).toUtf8()); connect(reply, &QNetworkReply::finished, this, [this, reply]() { - onTokenReply(reply); + handleTokenResponse(reply->readAll(), m_email, "outlook"); + reply->deleteLater(); }); } -void OutlookAuthenticator::onTokenReply(QNetworkReply *reply) -{ - if (reply->error() == QNetworkReply::NoError) { - QByteArray data = reply->readAll(); - handleTokenResponse(data, m_email, "outlook"); - } else { - QByteArray responseData = reply->readAll(); - qWarning() << "[OutlookAuthenticator] Token request failed:" - << reply->errorString() - << "Response:" << QString::fromUtf8(responseData); - emit authenticationFailed("Outlook token request failed: " + reply->errorString()); - } - reply->deleteLater(); -} - -#include "outlookauthenticator.moc" \ No newline at end of file +#include "outlookauthenticator.moc" diff --git a/src/ui/maillistview.cpp b/src/ui/maillistview.cpp index b57b101..c9dbc4b 100644 --- a/src/ui/maillistview.cpp +++ b/src/ui/maillistview.cpp @@ -59,6 +59,12 @@ void MailListView::setupUI() { m_tableView->setModel(m_proxyModel); connect(m_tableView, &QTableView::clicked, this, &MailListView::onRowSelected); + connect(m_tableView, &QTableView::doubleClicked, this, [this](const QModelIndex &index) { + if (!index.isValid()) return; + QModelIndex sourceIndex = m_proxyModel->mapToSource(index); + int mailId = sourceIndex.data(EmailListModel::IdRole).toInt(); + emit emailOpenRequested(mailId); + }); layout->addWidget(m_tableView); } diff --git a/src/ui/maillistview.h b/src/ui/maillistview.h index 815038f..9bf7b1f 100644 --- a/src/ui/maillistview.h +++ b/src/ui/maillistview.h @@ -21,6 +21,7 @@ public: signals: void emailSelected(int mailId); + void emailOpenRequested(int mailId); void composeRequested(); private slots: diff --git a/src/ui/mainmainwindow.cpp b/src/ui/mainmainwindow.cpp index 23810f3..fa089c1 100644 --- a/src/ui/mainmainwindow.cpp +++ b/src/ui/mainmainwindow.cpp @@ -164,12 +164,21 @@ void MainMainWindow::setupMailPage() { m_mailListView = new MailListView(); connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected); connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested); + connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) { + openMailInIndependentWindow(mailId); + }); m_folderSplitter->addWidget(m_mailListView); // Reader m_emailViewer = new ReaderView(); m_emailViewer->setMinimumWidth(350); connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested); + connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() { + std::optional currentItem = MailItemDao::findById(m_emailModel->getCurrentMailId()); + if (currentItem) { + openMailInIndependentWindow(currentItem->id()); + } + }); m_folderSplitter->addWidget(m_emailViewer); // Default sizes: folder 240, list 380, reader flex @@ -248,6 +257,24 @@ void MainMainWindow::onNewMessage() { switchToPage(PageCompose); } +void MainMainWindow::openMailInIndependentWindow(int mailId) { + std::optional item = MailItemDao::findById(mailId); + if (!item.has_value()) return; + + QMainWindow *detachedWin = new QMainWindow(); + detachedWin->setWindowTitle(QString("Mail - %1").arg(item->subject())); + + ReaderView *detachedReader = new ReaderView(); + detachedReader->setMailItem(&item.value()); + + detachedWin->setCentralWidget(detachedReader); + detachedWin->resize(800, 600); + detachedWin->setAttribute(Qt::WA_DeleteOnClose); + detachedWin->show(); + + statusBar()->showMessage("Opened mail in independent window", 3000); +} + void MainMainWindow::createToolBar() { m_toolBar = addToolBar("Main Toolbar"); m_toolBar->setMovable(false); diff --git a/src/ui/mainmainwindow.h b/src/ui/mainmainwindow.h index 72bdcf8..89ced9c 100644 --- a/src/ui/mainmainwindow.h +++ b/src/ui/mainmainwindow.h @@ -34,6 +34,7 @@ private slots: void onComposeRequested(); void onReaderReplyRequested(const MailItem *item); void onNewMessage(); + void openMailInIndependentWindow(int mailId); private: void setupUI(); diff --git a/src/ui/readerview.cpp b/src/ui/readerview.cpp index 99a1636..656e922 100644 --- a/src/ui/readerview.cpp +++ b/src/ui/readerview.cpp @@ -40,9 +40,13 @@ void ReaderView::setupUI() { m_forwardButton = new QPushButton("Forward"); m_deleteButton = new QPushButton("Delete"); m_deleteButton->setStyleSheet("color: red;"); - + + QPushButton *m_detachButton = new QPushButton("独立 (Independent)"); + m_detachButton->setToolTip("Open in separate window"); + actionsLayout->addWidget(m_replyButton); actionsLayout->addWidget(m_forwardButton); + actionsLayout->addWidget(m_detachButton); actionsLayout->addStretch(); actionsLayout->addWidget(m_deleteButton); @@ -59,6 +63,8 @@ void ReaderView::setupUI() { connect(m_replyButton, &QPushButton::clicked, [this]() { if (m_bodyViewer->toPlainText().isEmpty()) return; }); + + connect(m_detachButton, &QPushButton::clicked, this, &ReaderView::detachRequested); } void ReaderView::setMailItem(const MailItem* item) { diff --git a/src/ui/readerview.h b/src/ui/readerview.h index 19eb03f..48749b9 100644 --- a/src/ui/readerview.h +++ b/src/ui/readerview.h @@ -21,6 +21,8 @@ signals: void replyRequested(const MailItem* item); void forwardRequested(const MailItem* item); void deleteRequested(const MailItem* item); + void detachRequested(); + private: void setupUI();