commit 92158f9f845cb2f28035f32836de70c148aa35b3 Author: Timothee Leclaire-Fournier Date: Fri Mar 8 12:10:58 2024 -0500 emailQt: Initial commit diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..93dbe00 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.27) +project(emailQt) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(CMAKE_CXX_STANDARD 23) + +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG -fsanitize=address,undefined") +set(CMAKE_LINKER_FLAGS_DEBUG "${CMAKE_LINKER_FLAGS_DEBUG} -fsanitize=address,undefined") + +find_package(Qt6 COMPONENTS Core Gui Widgets REQUIRED) +find_package(Qt6Keychain CONFIG REQUIRED) + +add_executable(emailQt main.cpp + EmailClient.hpp + EmailClient.cpp + CurlHandler.hpp + CurlHandler.cpp + ParametersProvider.cpp + ParametersProvider.hpp + Email.cpp + Email.hpp + EmailDetails.cpp + EmailDetails.hpp) + +target_link_libraries(emailQt PRIVATE Qt6::Core + Qt6::Gui + Qt6::Widgets + curl + Qt6Keychain::Qt6Keychain) diff --git a/CurlHandler.cpp b/CurlHandler.cpp new file mode 100755 index 0000000..d1a5b2a --- /dev/null +++ b/CurlHandler.cpp @@ -0,0 +1,101 @@ +#include "CurlHandler.hpp" + +CurlHandler::CurlHandler() + : initialRes{}, initialCurl(curl_easy_init(), curl_easy_cleanup) { +} + +CurlHandler::~CurlHandler() { + for (auto& t : v) + t.join(); +} + +void CurlHandler::configure(const ParametersProvider::settings &sett) { + url.append("imaps://"); + url += sett.imapS; + url += "/INBOX?ALL"; + setts = sett; + + setupCurl(initialCurl, initialChunk, url); +} + +std::vector *CurlHandler::fetch() { + initialRes = curl_easy_perform(initialCurl.get()); + if (initialRes != CURLE_OK) { + return &decodedMessages; + } + + // We prepare to query the server. + url.erase(url.end() - 4, url.end()); + url += "/;MAILINDEX="; + + std::string temp; + std::stringstream s(initialChunk); + + size_t count = 0; + while (getline(s, temp, ' ')) { + // SELECT and * + if (count < 2) { + count++; + continue; + } + messagesId.emplace_back(temp); + } + + // We will query the server number by number. + // The messagesId vector contains all Email GUIDs. + v.reserve(messagesId.size()); + decodedMessages.resize(messagesId.size()); + count = 0; + + for (auto i{messagesId.rbegin()}; i != messagesId.rbegin() + 5; i++) + v.emplace_back(&CurlHandler::query, this, std::ref(*i), count++); + + return &decodedMessages; +} + +void CurlHandler::query(std::string &c, size_t count) { + std::shared_ptr localCurl(curl_easy_init(), curl_easy_cleanup); + CURLcode localCode; + c.erase(std::remove(c.begin(), c.end(), '\r'), c.end()); + c.erase(std::remove(c.begin(), c.end(), '\n'), c.end()); + setupCurl(localCurl, decodedMessages[count], url + c); + + localCode = curl_easy_perform(localCurl.get()); + if (localCode != CURLE_OK) { + // ... + } + + emit threadFinished(count, std::stoi(c)); +} + +void CurlHandler::setupCurl(std::shared_ptr ptr, std::string &m, std::string const &ur) const { + curl_easy_setopt(ptr.get(), CURLOPT_USERNAME, setts.userS.c_str()); + curl_easy_setopt(ptr.get(), CURLOPT_PASSWORD, setts.passS.c_str()); + + curl_easy_setopt(ptr.get(), CURLOPT_PORT, setts.port); + curl_easy_setopt(ptr.get(), CURLOPT_TIMEOUT, curlTimeoutSeconds); + + // The callback function will receive the string ref as its userdata + // and therefore save the data there. + curl_easy_setopt(ptr.get(), CURLOPT_USE_SSL, (long) CURLUSESSL_ALL); + curl_easy_setopt(ptr.get(), CURLOPT_WRITEDATA, (void *) &m); + curl_easy_setopt(ptr.get(), CURLOPT_WRITEFUNCTION, cb); + + // Necessary on some servers + curl_easy_setopt(ptr.get(), CURLOPT_USERAGENT, "libcurl-agent/1.0"); + + curl_easy_setopt(ptr.get(), CURLOPT_URL, ur.c_str()); + +#ifdef DEBUG + curl_easy_setopt(ptr.get(), CURLOPT_VERBOSE, 1L); +#else + curl_easy_setopt(ptr.get(), CURLOPT_VERBOSE, 0L); +#endif +} + +size_t cb(char *data, size_t size, size_t numberOfMembers, void *userdata) { + size_t realSize = size * numberOfMembers; + auto *mem = (std::string *) userdata; + mem->append(data, realSize); + return realSize; +} diff --git a/CurlHandler.hpp b/CurlHandler.hpp new file mode 100755 index 0000000..6463d90 --- /dev/null +++ b/CurlHandler.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "ParametersProvider.hpp" + +constexpr long curlTimeoutSeconds{5}; + +// View RFC 3501 and RFC 2192 for more details + +size_t cb(char *data, size_t size, size_t numberOfMembers, void *userdata); + +class CurlHandler : public QObject { + Q_OBJECT + +public: + CurlHandler(); + ~CurlHandler(); + + void configure(const ParametersProvider::settings &sett); + std::vector *fetch(); + + void query(std::string &c, size_t count); + void setupCurl(std::shared_ptr ptr, std::string &m, const std::string &ur) const; + +signals: + void threadFinished(size_t threadId, int uid); + +private: + std::string url; + std::shared_ptr initialCurl; + CURLcode initialRes; + std::string initialChunk; + + std::vector decodedMessages; + std::vector messagesId; + std::vector v; + + ParametersProvider::settings setts; +}; \ No newline at end of file diff --git a/Email.cpp b/Email.cpp new file mode 100644 index 0000000..a78130d --- /dev/null +++ b/Email.cpp @@ -0,0 +1,47 @@ +#include "Email.hpp" + +Email::Email(const std::string &str, size_t uid) + : QListWidgetItem(nullptr, QListWidgetItem::UserType), uid{uid} { + setString(str); +} + +Email::Email(Email const &e) + : QListWidgetItem(e), uid{e.uid}, titleS(e.titleS), messageS(e.messageS) { + setString(titleS); +} + +void Email::setString(const std::string &str) { + std::stringstream ss(str); + + std::string line; + while (std::getline(ss, line)) { + if (line.starts_with("Subject:")) + titleS = line.substr(9); + else if (line.starts_with("Content-Type:")) + parseEmailBody(ss, line); + } + + setText(QString::fromStdString(titleS)); +} + +const std::string &Email::title() const { + return titleS; +} + +const std::string &Email::message() const { + return messageS; +} + +bool Email::operator<(QListWidgetItem const &other) const { + auto a{dynamic_cast(other)}; + return this->uid < a.uid; +} + +void Email::parseEmailBody(std::stringstream &ss, std::string &line) { + if (!line.contains("text/plain")) + return; + + while (std::getline(ss, line) && !(line.starts_with("--"))) { + messageS.append(line); + } +} \ No newline at end of file diff --git a/Email.hpp b/Email.hpp new file mode 100644 index 0000000..430e008 --- /dev/null +++ b/Email.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + +#include +#include + +class Email : public QListWidgetItem { +public: + explicit Email(const std::string &str, size_t uid); + Email(const Email &e); + ~Email() = default; + + void setString(const std::string &str); + [[nodiscard]] const std::string &title() const; + [[nodiscard]] const std::string &message() const; + + bool operator<(const QListWidgetItem &other) const; + + void parseEmailBody(std::stringstream &ss, std::string &line); + +private: + std::string titleS; + std::string messageS; + size_t uid; +}; diff --git a/EmailClient.cpp b/EmailClient.cpp new file mode 100755 index 0000000..5947a85 --- /dev/null +++ b/EmailClient.cpp @@ -0,0 +1,44 @@ +#include "EmailClient.hpp" + +EmailClient::EmailClient(QWidget *parent) + : QMainWindow(parent) { + connect(&provider, &ParametersProvider::done, this, &EmailClient::parametersDone); + + auto *layout{new QVBoxLayout}; + response = new QListWidget; + + layout->addWidget(response); + + auto *centralWidget{new QWidget(this)}; + centralWidget->setLayout(layout); + setCentralWidget(centralWidget); + + setWindowTitle("emailQt"); + + connect(response, &QListWidget::itemActivated, this, &EmailClient::itemActivated); + connect(&handler, &CurlHandler::threadFinished, this, &EmailClient::updateList); +} + +void EmailClient::start() { + provider.show(); + provider.setFocusInternal(); +} + +void EmailClient::parametersDone() { + handler.configure(provider.getSettings()); + + emailsString = handler.fetch(); + + showMaximized(); +} + +void EmailClient::itemActivated(QListWidgetItem *item) { + auto *p = dynamic_cast(item); + det.setMail(*p); + det.show(); +} + +void EmailClient::updateList(size_t threadId, int uid) { + response->addItem(new Email((*emailsString)[threadId], uid)); + response->sortItems(Qt::DescendingOrder); +} diff --git a/EmailClient.hpp b/EmailClient.hpp new file mode 100755 index 0000000..01d97f1 --- /dev/null +++ b/EmailClient.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "CurlHandler.hpp" +#include "Email.hpp" +#include "EmailDetails.hpp" +#include "ParametersProvider.hpp" + +class EmailClient : public QMainWindow { + Q_OBJECT + +public: + explicit EmailClient(QWidget *parent = nullptr); + ~EmailClient() = default; + + void start(); + +public slots: + void parametersDone(); + void itemActivated(QListWidgetItem *item); + void updateList(size_t threadId, int uid); + +private: + ParametersProvider provider; + CurlHandler handler; + QListWidget *response; + + std::vector *emailsString{}; + QList emailTitles; + EmailDetails det; +}; diff --git a/EmailDetails.cpp b/EmailDetails.cpp new file mode 100644 index 0000000..7de8c0e --- /dev/null +++ b/EmailDetails.cpp @@ -0,0 +1,18 @@ +#include "EmailDetails.hpp" + +EmailDetails::EmailDetails(QWidget *parent) + : QWidget(parent) { + t = new QTextEdit; + + auto *lay = new QVBoxLayout; + + lay->addWidget(t); + setLayout(lay); + + resize(800, 600); +} + +void EmailDetails::setMail(const Email &m) { + setWindowTitle(QString::fromStdString(m.title())); + t->setPlainText(QString::fromStdString(m.message())); +} diff --git a/EmailDetails.hpp b/EmailDetails.hpp new file mode 100644 index 0000000..b84bd4e --- /dev/null +++ b/EmailDetails.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include + +#include "Email.hpp" + +class EmailDetails : public QWidget { + Q_OBJECT + +public: + explicit EmailDetails(QWidget *parent = nullptr); + + void setMail(const Email &m); + +private: + QTextEdit *t; +}; diff --git a/ParametersProvider.cpp b/ParametersProvider.cpp new file mode 100644 index 0000000..f3ea995 --- /dev/null +++ b/ParametersProvider.cpp @@ -0,0 +1,57 @@ +#include "ParametersProvider.hpp" + +ParametersProvider::ParametersProvider(QWidget *parent) + : QDialog(parent) { + auto *layout = new QVBoxLayout; + + auto *user = new QLabel("Nom d'utilisateur", this); + userField = new QLineEdit; + + auto *pass = new QLabel("Mot de passe", this); + passField = new QLineEdit; + passField->setEchoMode(QLineEdit::Password); + + auto *imap = new QLabel("Serveur IMAP", this); + imapField = new QLineEdit("imap.gmail.com"); + + auto *port = new QLabel("Port", this); + portField = new QLineEdit("993"); + portField->setValidator(new QIntValidator(1, 1000, this)); + + auto *startButton = new QPushButton("OK", this); + + layout->addWidget(user); + layout->addWidget(userField); + layout->addWidget(pass); + layout->addWidget(passField); + layout->addWidget(imap); + layout->addWidget(imapField); + layout->addWidget(port); + layout->addWidget(portField); + layout->addStretch(1); + layout->addWidget(startButton); + + this->setLayout(layout); + + resize(300, 450); + + connect(startButton, &QPushButton::clicked, this, &ParametersProvider::start); +} + +void ParametersProvider::start() { + sett.port = portField->text().toInt(); + sett.userS = userField->text().toStdString(); + sett.passS = passField->text().toStdString(); + sett.imapS = imapField->text().toStdString(); + close(); + emit done(); +} + +void ParametersProvider::setFocusInternal() { + activateWindow(); + userField->setFocus(); +} + +const ParametersProvider::settings &ParametersProvider::getSettings() { + return sett; +} diff --git a/ParametersProvider.hpp b/ParametersProvider.hpp new file mode 100644 index 0000000..7b3d43f --- /dev/null +++ b/ParametersProvider.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +class ParametersProvider : public QDialog { + Q_OBJECT + +public: + struct settings { + std::string userS, passS, imapS; + int port; + }; + + explicit ParametersProvider(QWidget *parent = nullptr); + ~ParametersProvider() = default; + + void start(); + void setFocusInternal(); + const settings &getSettings(); + +signals: + void done(); + +private: + settings sett; + QLineEdit *userField, *passField, *imapField, *portField; +}; diff --git a/README.md b/README.md new file mode 100644 index 0000000..19d1dee --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# emailQt - A small & simple Qt email client + +![](demo.mkv) + +## Usage +If you are using gmail, you need to enable 2FA and then generate an application password. +This can be done at https://myaccount.google.com/security. + +## Building +You will need a C++23 compiler, cmake, Qt6, libcurl and [QtKeychain](https://github.com/frankosterfeld/qtkeychain). \ No newline at end of file diff --git a/demo.mkv b/demo.mkv new file mode 100644 index 0000000..3ea417b Binary files /dev/null and b/demo.mkv differ diff --git a/main.cpp b/main.cpp new file mode 100755 index 0000000..4586b62 --- /dev/null +++ b/main.cpp @@ -0,0 +1,10 @@ +#include "EmailClient.hpp" +#include + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + EmailClient w; + w.start(); + return a.exec(); +}