[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);