[pim/messagelib] messagecomposer: Allow sending mail to all-unicode email addresses.

Carl Schwan <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit e948edb47a5f56a110615fa2acfc36595be9b823 by Carl Schwan, on behalf of Arnt Gulbrandsen.
Committed on 17/07/2026 at 16:25.
Pushed by carlschwan into branch 'master'.

Allow sending mail to all-unicode email addresses.

kmail and kmstp both have the necessary support, this closes the gap.

M  +53   -0    messagecomposer/autotests/skeletonmessagejobtest.cpp
M  +2    -0    messagecomposer/autotests/skeletonmessagejobtest.h
M  +9    -7    messagecomposer/src/composer/composerviewbase.cpp
M  +21   -13   messagecomposer/src/job/skeletonmessagejob.cpp
M  +4    -3    messagecomposer/src/sender/akonadisender.cpp

https://invent.kde.org/pim/messagelib/-/commit/e948edb47a5f56a110615fa2acfc36595be9b823

diff --git a/messagecomposer/autotests/skeletonmessagejobtest.cpp b/messagecomposer/autotests/skeletonmessagejobtest.cpp
index cba330763..abca8e6f8 100644
--- a/messagecomposer/autotests/skeletonmessagejobtest.cpp
+++ b/messagecomposer/autotests/skeletonmessagejobtest.cpp
@@ -9,7 +9,9 @@
 #include <QDebug>
 #include <QTest>
 
+#include <KMime/Headers>
 #include <KMime/Message>
+#include <KMime/Types>
 
 #include <MessageComposer/ComposerJob>
 #include <MessageComposer/GlobalPart>
@@ -190,6 +192,57 @@ void SkeletonMessageJobTest::testAddresses()
     }
 }
 
+void SkeletonMessageJobTest::testEaiAddresses_data()
+{
+    QTest::addColumn<QString>("input");
+    QTest::addColumn<QString>("expectedLocalPart");
+    QTest::addColumn<QString>("expectedDomain");
+
+    // A fully internationalised address (RFC6531) must survive composition as
+    // raw UTF-8: the localpart may not become an RFC2047 encoded-word (those
+    // are forbidden inside an addr-spec), and the domain may not become an
+    // xn-- a-label (that is a DNS wire form, not for a header a human reads).
+    QTest::newRow("localpart and domain") << u"Grå katt <grå@grå.org>"_s << u"grå"_s << u"grå.org"_s;
+    QTest::newRow("localpart only") << u"Gøril <gø[email protected]>"_s << u"gøril"_s << u"example.com"_s;
+    QTest::newRow("domain only") << u"Arnt <arnt@grå.org>"_s << u"arnt"_s << u"grå.org"_s;
+}
+
+void SkeletonMessageJobTest::testEaiAddresses()
+{
+    ComposerJob composerJob;
+    InfoPart *infoPart = composerJob.infoPart();
+    GlobalPart *globalPart = composerJob.globalPart();
+    Q_ASSERT(infoPart);
+
+    QFETCH(QString, input);
+    QFETCH(QString, expectedLocalPart);
+    QFETCH(QString, expectedDomain);
+
+    infoPart->setFrom(input);
+    infoPart->setTo(QStringList{input});
+    auto sjob = new SkeletonMessageJob(infoPart, globalPart, &composerJob);
+    QVERIFY(sjob->exec());
+    const auto message = sjob->takeMessage();
+
+    const QByteArray wantAddr = '<' + (expectedLocalPart + u'@' + expectedDomain).toUtf8() + '>';
+    const auto check = [&](const QList<KMime::Types::Mailbox> &mailboxes, const QByteArray &raw) {
+        QCOMPARE(mailboxes.size(), 1);
+        const auto addrSpec = mailboxes.first().addrSpec();
+        QCOMPARE(addrSpec.localPart, expectedLocalPart);
+        QCOMPARE(addrSpec.domain, expectedDomain);
+        // And the same thing as bytes on the wire: the addr-spec verbatim in
+        // UTF-8, with no a-label anywhere in the header.
+        QVERIFY(!raw.contains("xn--"));
+        QVERIFY(raw.contains(wantAddr));
+    };
+
+    QVERIFY(message->from(KMime::CreatePolicy::DontCreate));
+    check(message->from()->mailboxes(), message->from()->as7BitString());
+
+    QVERIFY(message->to(KMime::CreatePolicy::DontCreate));
+    check(message->to()->mailboxes(), message->to()->as7BitString());
+}
+
 void SkeletonMessageJobTest::testMessageID()
 {
     ComposerJob composerJob;
diff --git a/messagecomposer/autotests/skeletonmessagejobtest.h b/messagecomposer/autotests/skeletonmessagejobtest.h
index 5b8860c4a..34a5fd556 100644
--- a/messagecomposer/autotests/skeletonmessagejobtest.h
+++ b/messagecomposer/autotests/skeletonmessagejobtest.h
@@ -16,5 +16,7 @@ private Q_SLOTS:
     void testSubject();
     void testAddresses_data();
     void testAddresses();
+    void testEaiAddresses_data();
+    void testEaiAddresses();
     void testMessageID();
 };
diff --git a/messagecomposer/src/composer/composerviewbase.cpp b/messagecomposer/src/composer/composerviewbase.cpp
index 80c308405..226658829 100644
--- a/messagecomposer/src/composer/composerviewbase.cpp
+++ b/messagecomposer/src/composer/composerviewbase.cpp
@@ -1134,23 +1134,25 @@ void ComposerViewBase::queueMessage(const std::shared_ptr<KMime::Message> &messa
     }
 
     MailTransport::Transport *transport = MailTransport::TransportManager::self()->transportById(infoPart->transportId());
+    // Pass the envelope addresses on as UTF-8. Whether the domain travels as an
+    // a-label or the whole address needs SMTPUTF8 depends on the server, so that
+    // decision belongs to the transport (ksmtp), which alone knows its EHLO reply.
     if (transport && transport->specifySenderOverwriteAddress()) {
-        qjob->addressAttribute().setFrom(
-            KEmailAddress::extractEmailAddress(KEmailAddress::normalizeAddressesAndEncodeIdn(transport->senderOverwriteAddress())));
+        qjob->addressAttribute().setFrom(KEmailAddress::extractEmailAddress(transport->senderOverwriteAddress()));
     } else {
-        qjob->addressAttribute().setFrom(KEmailAddress::extractEmailAddress(KEmailAddress::normalizeAddressesAndEncodeIdn(infoPart->from())));
+        qjob->addressAttribute().setFrom(KEmailAddress::extractEmailAddress(infoPart->from()));
     }
     // if this header is not empty, it contains the real recipient of the message, either the primary or one of the
     //  secondary recipients. so we set that to the transport job, while leaving the message itself alone.
     if (KMime::Headers::Base *realTo = message->headerByType("X-KMail-EncBccRecipients")) {
-        qjob->addressAttribute().setTo(MessageComposer::Util::cleanUpEmailListAndEncoding(realTo->asUnicodeString().split(u'%')));
+        qjob->addressAttribute().setTo(MessageComposer::Util::cleanEmailList(realTo->asUnicodeString().split(u'%')));
         message->removeHeader("X-KMail-EncBccRecipients");
         message->assemble();
         qCDebug(MESSAGECOMPOSER_LOG) << "sending with-bcc encr mail to a/n recipient:" << qjob->addressAttribute().to();
     } else {
-        qjob->addressAttribute().setTo(MessageComposer::Util::cleanUpEmailListAndEncoding(infoPart->to()));
-        qjob->addressAttribute().setCc(MessageComposer::Util::cleanUpEmailListAndEncoding(infoPart->cc()));
-        qjob->addressAttribute().setBcc(MessageComposer::Util::cleanUpEmailListAndEncoding(infoPart->bcc()));
+        qjob->addressAttribute().setTo(MessageComposer::Util::cleanEmailList(infoPart->to()));
+        qjob->addressAttribute().setCc(MessageComposer::Util::cleanEmailList(infoPart->cc()));
+        qjob->addressAttribute().setBcc(MessageComposer::Util::cleanEmailList(infoPart->bcc()));
     }
     if (m_requestDeleveryConfirmation) {
         qjob->addressAttribute().setDeliveryStatusNotification(true);
diff --git a/messagecomposer/src/job/skeletonmessagejob.cpp b/messagecomposer/src/job/skeletonmessagejob.cpp
index 5a2651312..521ced981 100644
--- a/messagecomposer/src/job/skeletonmessagejob.cpp
+++ b/messagecomposer/src/job/skeletonmessagejob.cpp
@@ -18,7 +18,6 @@ using namespace Qt::Literals::StringLiterals;
 
 #include "messagecomposer_debug.h"
 
-#include <KEmailAddress>
 #include <KMime/Message>
 
 using namespace MessageComposer;
@@ -49,12 +48,12 @@ void SkeletonMessageJobPrivate::doStart()
     message = std::make_unique<KMime::Message>();
 
     KMime::Types::Mailbox fromAddress;
-    fromAddress.fromUnicodeString(KEmailAddress::normalizeAddressesAndEncodeIdn(infoPart->from()));
+    fromAddress.fromUnicodeString(infoPart->from());
 
     // From:
     {
         auto from = std::make_unique<KMime::Headers::From>();
-        from->fromUnicodeString(QString::fromUtf8(fromAddress.as7BitString("utf-8")));
+        from->from7BitString(fromAddress.as7BitString("utf-8"));
         message->setHeader(std::move(from));
     }
 
@@ -65,13 +64,13 @@ void SkeletonMessageJobPrivate::doStart()
         const QStringList lstTo = infoPart->to();
         for (const QString &a : lstTo) {
             KMime::Types::Mailbox address;
-            address.fromUnicodeString(KEmailAddress::normalizeAddressesAndEncodeIdn(a));
+            address.fromUnicodeString(a);
             if (!sTo.isEmpty()) {
                 sTo.append(",");
             }
             sTo.append(address.as7BitString("utf-8"));
         }
-        to->fromUnicodeString(QString::fromUtf8(sTo));
+        to->from7BitString(sTo);
         message->setHeader(std::move(to));
     }
 
@@ -82,13 +81,13 @@ void SkeletonMessageJobPrivate::doStart()
         QByteArray sReplyTo;
         for (const QString &a : lstReplyTo) {
             KMime::Types::Mailbox address;
-            address.fromUnicodeString(KEmailAddress::normalizeAddressesAndEncodeIdn(a));
+            address.fromUnicodeString(a);
             if (!sReplyTo.isEmpty()) {
                 sReplyTo.append(",");
             }
             sReplyTo.append(address.as7BitString("utf-8"));
         }
-        replyTo->fromUnicodeString(QString::fromUtf8(sReplyTo));
+        replyTo->from7BitString(sReplyTo);
         message->setHeader(std::move(replyTo));
     }
 
@@ -99,13 +98,13 @@ void SkeletonMessageJobPrivate::doStart()
         const QStringList lstCc = infoPart->cc();
         for (const QString &a : lstCc) {
             KMime::Types::Mailbox address;
-            address.fromUnicodeString(KEmailAddress::normalizeAddressesAndEncodeIdn(a));
+            address.fromUnicodeString(a);
             if (!sCc.isEmpty()) {
                 sCc.append(",");
             }
             sCc.append(address.as7BitString("utf-8"));
         }
-        cc->fromUnicodeString(QString::fromUtf8(sCc));
+        cc->from7BitString(sCc);
         message->setHeader(std::move(cc));
     }
 
@@ -116,13 +115,13 @@ void SkeletonMessageJobPrivate::doStart()
         const QStringList lstBcc = infoPart->bcc();
         for (const QString &a : lstBcc) {
             KMime::Types::Mailbox address;
-            address.fromUnicodeString(KEmailAddress::normalizeAddressesAndEncodeIdn(a));
+            address.fromUnicodeString(a);
             if (!sBcc.isEmpty()) {
                 sBcc.append(",");
             }
             sBcc.append(address.as7BitString("utf-8"));
         }
-        bcc->fromUnicodeString(QString::fromUtf8(sBcc));
+        bcc->from7BitString(sBcc);
         message->setHeader(std::move(bcc));
     }
 
@@ -157,9 +156,18 @@ void SkeletonMessageJobPrivate::doStart()
 
     // Message-ID
     {
-        const auto fromParts = infoPart->from();
         auto messageId = std::make_unique<KMime::Headers::MessageID>();
-        messageId->generate(fromAddress.addrSpec().domain.toUtf8());
+        // The address headers keep the domain as UTF-8, but a
+        // Message-ID is machine-read and may be copied into later,
+        // ASCII-only messages, so it's best to stay with ASCII
+        // here. Needing SMTPUTF8 for an address is one thing, needing
+        // it just for a message-id quite another.
+        const QString domain = fromAddress.addrSpec().domain;
+        QByteArray idnDomain = QUrl::toAce(domain);
+        if (idnDomain.isEmpty()) {
+            idnDomain = domain.toUtf8();
+        }
+        messageId->generate(idnDomain);
         message->setHeader(std::move(messageId));
     }
     // Extras
diff --git a/messagecomposer/src/sender/akonadisender.cpp b/messagecomposer/src/sender/akonadisender.cpp
index 4725835f7..138bc324b 100644
--- a/messagecomposer/src/sender/akonadisender.cpp
+++ b/messagecomposer/src/sender/akonadisender.cpp
@@ -187,11 +187,12 @@ void AkonadiSender::sendOrQueueMessage(const std::shared_ptr<KMime::Message> &me
         return;
     }
 
+    // Envelope addresses stay UTF-8; the transport (ksmtp) decides on a-label
+    // versus SMTPUTF8 once it knows what the server announced in its EHLO reply.
     if (transport && transport->specifySenderOverwriteAddress()) {
-        qjob->addressAttribute().setFrom(
-            KEmailAddress::extractEmailAddress(KEmailAddress::normalizeAddressesAndEncodeIdn(transport->senderOverwriteAddress())));
+        qjob->addressAttribute().setFrom(KEmailAddress::extractEmailAddress(transport->senderOverwriteAddress()));
     } else {
-        qjob->addressAttribute().setFrom(KEmailAddress::extractEmailAddress(KEmailAddress::normalizeAddressesAndEncodeIdn(message->from()->asUnicodeString())));
+        qjob->addressAttribute().setFrom(KEmailAddress::extractEmailAddress(message->from()->asUnicodeString()));
     }
 
     MessageComposer::Util::addSendReplyForwardAction(message, qjob);
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.