emailQt: Initial commit

This commit is contained in:
Timothée Leclaire-Fournier 2024-03-08 12:10:58 -05:00
commit 92158f9f84
14 changed files with 490 additions and 0 deletions

32
CMakeLists.txt Normal file
View File

@ -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)

101
CurlHandler.cpp Executable file
View File

@ -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<std::string> *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<CURL> 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<CURL> 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;
}

49
CurlHandler.hpp Executable file
View File

@ -0,0 +1,49 @@
#pragma once
#include <cstdlib>
#include <functional>
#include <memory>
#include <string>
#include <thread>
#include <vector>
#include <QObject>
#include <curl/curl.h>
#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<std::string> *fetch();
void query(std::string &c, size_t count);
void setupCurl(std::shared_ptr<CURL> ptr, std::string &m, const std::string &ur) const;
signals:
void threadFinished(size_t threadId, int uid);
private:
std::string url;
std::shared_ptr<CURL> initialCurl;
CURLcode initialRes;
std::string initialChunk;
std::vector<std::string> decodedMessages;
std::vector<std::string> messagesId;
std::vector<std::thread> v;
ParametersProvider::settings setts;
};

47
Email.cpp Normal file
View File

@ -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<const Email &>(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);
}
}

28
Email.hpp Normal file
View File

@ -0,0 +1,28 @@
#pragma once
#include <sstream>
#include <string>
#include <unordered_map>
#include <QListWidgetItem>
#include <QString>
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;
};

44
EmailClient.cpp Executable file
View File

@ -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<Email *>(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);
}

43
EmailClient.hpp Executable file
View File

@ -0,0 +1,43 @@
#pragma once
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
#include <QListWidget>
#include <QList>
#include <QMainWindow>
#include <QString>
#include <QTextEdit>
#include <QVBoxLayout>
#include <QWidget>
#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<std::string> *emailsString{};
QList<QString> emailTitles;
EmailDetails det;
};

18
EmailDetails.cpp Normal file
View File

@ -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()));
}

19
EmailDetails.hpp Normal file
View File

@ -0,0 +1,19 @@
#pragma once
#include <QTextEdit>
#include <QVBoxLayout>
#include <QWidget>
#include "Email.hpp"
class EmailDetails : public QWidget {
Q_OBJECT
public:
explicit EmailDetails(QWidget *parent = nullptr);
void setMail(const Email &m);
private:
QTextEdit *t;
};

57
ParametersProvider.cpp Normal file
View File

@ -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;
}

32
ParametersProvider.hpp Normal file
View File

@ -0,0 +1,32 @@
#pragma once
#include <QDialog>
#include <QIntValidator>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QVBoxLayout>
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;
};

10
README.md Normal file
View File

@ -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).

BIN
demo.mkv Normal file

Binary file not shown.

10
main.cpp Executable file
View File

@ -0,0 +1,10 @@
#include "EmailClient.hpp"
#include <QtWidgets/QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
EmailClient w;
w.start();
return a.exec();
}