diff --git a/CMakeLists.txt b/CMakeLists.txt index 99c8856..50e9be8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,7 @@ set(SRC_FILES src/core/eventbus.cpp src/utils/notificationmanager.cpp src/core/emailmanager.cpp + src/core/emailcomposerbridge.cpp src/core/synchronizerprovider.cpp src/syncscheduler.cpp src/core/accountsetupdialoglauncher.cpp @@ -76,7 +77,7 @@ set(SRC_FILES add_executable(wino-mail-qt ${SRC_FILES}) # Link Qt - target_link_libraries(wino-mail-qt PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Network Qt6::Sql Qt6::WebEngineWidgets Qt6::Qml Qt6::Quick Qt6::QuickControls2 Qt6::Multimedia) + target_link_libraries(wino-mail-qt PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Network Qt6::Sql Qt6::WebEngineWidgets Qt6::Qml Qt6::Quick Qt6::Multimedia) # Link DTKWidget if found if(USE_DTKWIDGET AND DTKWIDGET_FOUND) @@ -84,8 +85,8 @@ if(USE_DTKWIDGET AND DTKWIDGET_FOUND) endif() # If using external libs -# target_link_libraries(wino-mail-qt PRIVATE ${LIBETPAN_LIBRARIES} ${GMIME_LIBRARIES}) -# target_include_directories(wino-mail-qt PRIVATE ${LIBETPAN_INCLUDE_DIRS} ${GMIME_INCLUDE_DIRS}) +target_link_libraries(wino-mail-qt PRIVATE ${GMIME_LIBRARIES}) +target_include_directories(wino-mail-qt PRIVATE ${GMIME_INCLUDE_DIRS}) # Install (optional) install(TARGETS wino-mail-qt DESTINATION bin) diff --git a/resources/qml/ComposePage.qml b/resources/qml/ComposePage.qml index 245d58f..3a68e5c 100644 --- a/resources/qml/ComposePage.qml +++ b/resources/qml/ComposePage.qml @@ -1,11 +1,16 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import QtWebEngine 1.15 +import QtQml 2.15 Item { id: composePage anchors.fill: parent + // Expose a bridge to C++ for sending email and getting Quill content + property var emailBridge: emailComposerBridge + // Back button Rectangle { anchors { @@ -69,17 +74,47 @@ Item { Layout.fillWidth: true } - // Body + // Body - WebEngineView loading editor.html with Quill Text { text: qsTr("Body:") font.pointSize: 14 Layout.alignment: Qt.AlignLeft } - TextArea { - id: bodyArea - placeholderText: qsTr("Write your message...") + WebEngineView { + id: bodyWebView Layout.fillWidth: true Layout.fillHeight: true + url: "qrc:/resources/web/editor.html" + // Settings to enable webchannel + settings.javascriptEnabled: true + settings.localContentCanAccessRemoteUrls: true + settings.localContentCanAccessFileUrls: true + } + + // Attachments area (simple list placeholder) + RowLayout { + id: attachmentsRow + Layout.fillWidth: true + spacing: 5 + // Will be populated dynamically via C++ model or JS array + // For now, placeholder + Text { + text: qsTr("Attachments: ") + font.pointSize: 12 + } + Text { + id: attachmentsText + text: qsTr("None") + font.pointSize: 12 + color: "#666" + } + Button { + text: qsTr("Attach") + onClicked: { + // Call C++ to open file dialog and get selected files + emailBridge.openAttachmentDialog() + } + } } // Send button @@ -93,10 +128,11 @@ Item { MouseArea { anchors.fill: parent onClicked: { - // TODO: Implement sending email - console.log("Sending email to:", toField.text, "subject:", subjectField.text) - // For now, just go back - StackView.view.pop() + // Trigger sending via bridge + emailBridge.sendComposedEmail( + toField.text, + subjectField.text + ) } } Text { diff --git a/src/core/emailmanager.cpp b/src/core/emailmanager.cpp index c1d2792..92b5bad 100644 --- a/src/core/emailmanager.cpp +++ b/src/core/emailmanager.cpp @@ -1,3 +1,20 @@ +#include +#include + +// Undefine macros that conflict with Qt's signals/slots +#ifdef public +#undef public +#endif +#ifdef signals +#undef signals +#endif +#ifdef slots +#undef slots +#endif +#ifdef emit +#undef emit +#endif + #include "emailmanager.h" #include #include @@ -5,6 +22,45 @@ #include #include +/* Helper: recursively find a part with given subtype */ +static GMimeObject *find_part_by_subtype(GMimeObject *obj, const char *subtype) +{ + if (!obj) + return nullptr; + + GMimeContentType *ctype = g_mime_object_get_content_type(obj); + if (ctype && + g_ascii_strcasecmp(g_mime_content_type_get_media_subtype(ctype), subtype) == 0) + return obj; + + if (GMIME_IS_MULTIPART(obj)) { + GMimeMultipart *multipart = GMIME_MULTIPART(obj); + guint count = g_mime_multipart_get_count(multipart); + for (guint i = 0; i < count; ++i) { + GMimeObject *part = g_mime_multipart_get_part(multipart, i); + GMimeObject *found = find_part_by_subtype(part, subtype); + if (found) + return found; + g_object_unref(part); + } + } + return nullptr; +} + +/* Helper: extract content as QString */ +static QString extract_content_as_string(GMimeObject *obj) +{ + GMimeStream *stream = g_mime_stream_mem_new(); + g_mime_object_write_to_stream(obj, nullptr, stream); + GMimeStreamMem *mem = GMIME_STREAM_MEM(stream); + GByteArray *array = g_mime_stream_mem_get_byte_array(mem); + const char *data = reinterpret_cast(array->data); + gsize size = array->len; + QString result = QString::fromUtf8(data, size); + g_object_unref(stream); + return result; +} + EmailManager::EmailManager(QObject *parent) : QObject(parent) { @@ -12,70 +68,86 @@ EmailManager::EmailManager(QObject *parent) MailItem EmailManager::getMailItemById(qint64 id) const { - // Use the DAO to fetch the MailItem by id auto optItem = MailItemDao::findById(id); - if (optItem.has_value()) { - return optItem.value(); - } - // Return an empty MailItem if not found - return MailItem(); + return optItem.has_value() ? optItem.value() : MailItem(); } QString EmailManager::getStorageDirectory() const { - // Define where .eml files are stored (e.g., in the application data directory) QString storagePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); QDir dir(storagePath); - if (!dir.exists()) { - dir.mkpath("."); // create if it doesn't exist - } - // Assuming .eml files are stored directly in this directory + if (!dir.exists()) + dir.mkpath("."); return storagePath; } QString EmailManager::getEmailHtmlById(qint64 id) const { - // Get the MailItem by id MailItem item = getMailItemById(id); - if (item.id() == 0) { + if (item.id() == 0) return "

Error: Email not found

"; - } - // Construct the file path: storage directory + fileId + .eml + QString storageDir = getStorageDirectory(); QString fileName = item.fileId(); - if (fileName.isEmpty()) { + if (fileName.isEmpty()) return "

Error: Email file ID is empty

"; - } + QString filePath = storageDir + QDir::separator() + fileName + ".eml"; - // Convert the .eml file to HTML return convertEmlToHtml(filePath); } QString EmailManager::convertEmlToHtml(const QString& emlFilePath) const { - // TODO: Implement actual MIME to HTML conversion using gmime or similar - // For now, return a placeholder indicating the feature is not yet implemented - QFile file(emlFilePath); - if (!file.exists()) { - return "

Error: Email file not found

"; + static bool initialized = false; + if (!initialized) { + g_mime_init(); + initialized = true; } - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + + FILE *fp = fopen(emlFilePath.toLocal8Bit().constData(), "r"); + if (!fp) return "

Error: Cannot open email file

"; + + GMimeStream *istream = g_mime_stream_fs_new(fileno(fp)); + GMimeParser *parser = g_mime_parser_new_with_stream(istream); + GMimeMessage *message = g_mime_parser_construct_message(parser, nullptr); + g_object_unref(parser); + g_object_unref(istream); + fclose(fp); + + if (!message) + return "

Error: Failed to parse email message

"; + + /* Try to get HTML part */ + GMimeObject *htmlPart = find_part_by_subtype(GMIME_OBJECT(message), "html"); + if (htmlPart) { + QString html = extract_content_as_string(htmlPart); + g_object_unref(htmlPart); + g_object_unref(message); + return html; } - QByteArray data = file.readAll(); - file.close(); - // Very basic conversion: just show raw content in a pre tag for now - QString content = QString::fromUtf8(data); - content.replace("&", "&").replace("<", "<").replace(">", ">"); - return QString("

Email Content (raw)

%1
") - .arg(content); + + /* Fallback to plain text */ + GMimeObject *textPart = find_part_by_subtype(GMIME_OBJECT(message), "plain"); + if (textPart) { + QString plain = extract_content_as_string(textPart); + g_object_unref(textPart); + g_object_unref(message); + QString escaped = plain; + escaped.replace("&", "&").replace("<", "<").replace(">", ">"); + return QString("
%1
") + .arg(escaped); + } + + /* No displayable part */ + g_object_unref(message); + return "

Error: No displayable content in email

"; } bool EmailManager::sendEmail(const QString& to, const QString& subject, const QString& body) { - // TODO: Implement actual email sending - // For now, just show a message and return true - QMessageBox::information(nullptr, "Send Email", + /* TODO: Implement real sending via RequestProcessor */ + QMessageBox::information(nullptr, "Send Email", QString("To: %1\nSubject: %2\nBody: %3").arg(to, subject, body)); return true; } \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 6b58f7b..0cd4d11 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,6 +4,7 @@ #include "core/translator.h" #include "db/dbchangeprocessor.h" #include "core/emailmanager.h" +#include "core/emailcomposerbridge.h" #include "syncscheduler.h" #include "utils/notificationmanager.h" #include "core/accountsetupdialoglauncher.h" @@ -24,6 +25,9 @@ int main(int argc, char *argv[]) // Create EmailManager to expose to QML EmailManager emailManager(&app); + // Create EmailComposerBridge to expose to QML + EmailComposerBridge emailComposerBridge(&app); + // Create and start the sync scheduler SyncScheduler syncScheduler(&app); syncScheduler.start(); @@ -38,6 +42,7 @@ int main(int argc, char *argv[]) QQmlApplicationEngine engine; engine.rootContext()->setContextProperty("translator", static_cast(&translator)); engine.rootContext()->setContextProperty("emailManager", &emailManager); + engine.rootContext()->setContextProperty("emailComposerBridge", &emailComposerBridge); engine.rootContext()->setContextProperty("syncScheduler", &syncScheduler); engine.rootContext()->setContextProperty("notificationManager", ¬ificationManager); engine.rootContext()->setContextProperty("accountSetupDialogLauncher", &accountSetupDialogLauncher);