File indexing completed on 2024-05-12 05:14:49

0001 /*
0002  *  kamail.cpp  -  email functions
0003  *  Program:  kalarm
0004  *  SPDX-FileCopyrightText: 2002-2022 David Jarvie <djarvie@kde.org>
0005  *
0006  *  SPDX-License-Identifier: GPL-2.0-or-later
0007  */
0008 
0009 #include "kamail.h"
0010 
0011 #include "kalarm.h"
0012 #include "kalarmapp.h"
0013 #include "mainwindow.h"
0014 #include "preferences.h"
0015 #include "lib/messagebox.h"
0016 #include "kalarmcalendar/identities.h"
0017 #include "akonadiplugin/akonadiplugin.h"
0018 #include "kalarm_debug.h"
0019 
0020 #include "kmailinterface.h"
0021 
0022 #include <KIdentityManagementCore/IdentityManager>
0023 #include <KIdentityManagementCore/Identity>
0024 #include <KMime/HeaderParsing>
0025 #include <KMime/Headers>
0026 #include <KMime/Message>
0027 
0028 #include <KEmailAddress>
0029 #include <KAboutData>
0030 #include <KLocalizedString>
0031 #include <KFileItem>
0032 #include <KIO/StatJob>
0033 #include <KIO/StoredTransferJob>
0034 #include <KJobWidgets>
0035 #include <KEMailSettings>
0036 #include <KCodecs>
0037 #include <KShell>
0038 
0039 #include <QUrl>
0040 #include <QFile>
0041 #include <QHostInfo>
0042 #include <QList>
0043 #include <QByteArray>
0044 #include <QTextCodec>
0045 #include <QStandardPaths>
0046 #include <QDBusInterface>
0047 
0048 #ifdef Q_OS_WIN
0049 #define popen _popen
0050 #define pclose _pclose
0051 #endif
0052 
0053 using namespace MailSend;
0054 
0055 namespace HeaderParsing
0056 {
0057 bool parseAddress(const char* & scursor, const char* const send,
0058                   KMime::Types::Address& result, bool isCRLF = false);
0059 }
0060 
0061 namespace
0062 {
0063 void                        initHeaders(KMime::Message&, JobData&);
0064 KMime::Types::Mailbox::List parseAddresses(const QString& text, QString& invalidItem);
0065 QString                     extractEmailAndNormalize(const QString& emailAddress);
0066 QByteArray                  autoDetectCharset(const QString& text);
0067 }
0068 
0069 QString KAMail::i18n_NeedFromEmailAddress()
0070 { return i18nc("@info", "A 'From' email address must be configured in order to execute email alarms."); }
0071 
0072 QString KAMail::i18n_sent_mail()
0073 { return i18nc("@info KMail folder name: this should be translated the same as in kmail", "sent-mail"); }
0074 
0075 KAMail*        KAMail::mInstance = nullptr;   // used only to enable signals/slots to work
0076 AkonadiPlugin* KAMail::mAkonadiPlugin = nullptr;
0077 
0078 KAMail* KAMail::instance()
0079 {
0080     if (!mInstance)
0081         mInstance = new KAMail();
0082     return mInstance;
0083 }
0084 
0085 /******************************************************************************
0086 * Send the email message specified in an event.
0087 * Reply = 1 if the message was sent - 'errmsgs' may contain copy error messages.
0088 *       = 0 if the message is queued for sending.
0089 *       = -1 if the message was not sent - 'errmsgs' contains the error messages.
0090 */
0091 int KAMail::send(JobData& jobdata, QStringList& errmsgs)
0092 {
0093     QString err;
0094     KIdentityManagementCore::Identity identity;
0095     jobdata.from = Preferences::emailAddress();
0096     if (jobdata.event.emailFromId()
0097     &&  Preferences::emailFrom() == Preferences::MAIL_FROM_KMAIL)
0098     {
0099         identity = Identities::identityManager()->identityForUoid(jobdata.event.emailFromId());
0100         if (identity.isNull())
0101         {
0102             qCCritical(KALARM_LOG) << "KAMail::send: Identity" << jobdata.event.emailFromId() << "not found";
0103             errmsgs = errors(xi18nc("@info", "Invalid 'From' email address.<nl/>Email identity <resource>%1</resource> not found", jobdata.event.emailFromId()));
0104             return -1;
0105         }
0106         if (identity.primaryEmailAddress().isEmpty())
0107         {
0108             qCCritical(KALARM_LOG) << "KAMail::send: Identity" << identity.identityName() << "uoid" << identity.uoid() << ": no email address";
0109             errmsgs = errors(xi18nc("@info", "Invalid 'From' email address.<nl/>Email identity <resource>%1</resource> has no email address", identity.identityName()));
0110             return -1;
0111         }
0112         jobdata.from = identity.fullEmailAddr();
0113     }
0114     if (jobdata.from.isEmpty())
0115     {
0116         switch (Preferences::emailFrom())
0117         {
0118             case Preferences::MAIL_FROM_KMAIL:
0119                 errmsgs = errors(xi18nc("@info", "<para>No 'From' email address is configured (no default email identity found)</para>"
0120                                                 "<para>Please set it in <application>KMail</application> or in the <application>KAlarm</application> Configuration dialog.</para>"));
0121                 break;
0122             case Preferences::MAIL_FROM_SYS_SETTINGS:
0123                 errmsgs = errors(xi18nc("@info", "<para>No 'From' email address is configured.</para>"
0124                                                 "<para>Please set a default address in <application>KMail</application> or in the <application>KAlarm</application> Configuration dialog.</para>"));
0125                 break;
0126             case Preferences::MAIL_FROM_ADDR:
0127             default:
0128                 errmsgs = errors(xi18nc("@info", "<para>No 'From' email address is configured.</para>"
0129                                                 "<para>Please set it in the <application>KAlarm</application> Configuration dialog.</para>"));
0130                 break;
0131         }
0132         return -1;
0133     }
0134     jobdata.bcc  = (jobdata.event.emailBcc() ? Preferences::emailBccAddress() : QString());
0135     qCDebug(KALARM_LOG) << "KAMail::send: To:" << jobdata.event.emailAddresses(QStringLiteral(","))
0136                         << "\nSubject:" << jobdata.event.emailSubject();
0137 
0138     KMime::Message::Ptr message = KMime::Message::Ptr(new KMime::Message);
0139 
0140     if (Preferences::emailClient() == Preferences::sendmail)
0141     {
0142         qCDebug(KALARM_LOG) << "KAMail::send: Sending via sendmail";
0143         const QStringList paths{QStringLiteral("/sbin"), QStringLiteral("/usr/sbin"), QStringLiteral("/usr/lib")};
0144         QString command = QStandardPaths::findExecutable(QStringLiteral("sendmail"), paths);
0145         if (!command.isNull())
0146         {
0147             command += QStringLiteral(" -f ");
0148             command += extractEmailAndNormalize(jobdata.from);
0149             command += QStringLiteral(" -oi -t ");
0150             initHeaders(*message, jobdata);
0151         }
0152         else
0153         {
0154             command = QStandardPaths::findExecutable(QStringLiteral("mail"), paths);
0155             if (command.isNull())
0156             {
0157                 qCCritical(KALARM_LOG) << "KAMail::send: sendmail not found";
0158                 errmsgs = errors(xi18nc("@info", "<command>%1</command> not found", QStringLiteral("sendmail"))); // give up
0159                 return -1;
0160             }
0161 
0162             command += QStringLiteral(" -s ");
0163             command += KShell::quoteArg(jobdata.event.emailSubject());
0164 
0165             if (!jobdata.bcc.isEmpty())
0166             {
0167                 command += QStringLiteral(" -b ");
0168                 command += extractEmailAndNormalize(jobdata.bcc);
0169             }
0170 
0171             command += QLatin1Char(' ');
0172             command += jobdata.event.emailPureAddresses(QStringLiteral(" ")); // locally provided, okay
0173         }
0174         // Add the body and attachments to the message.
0175         // (Sendmail requires attachments to have already been included in the message.)
0176         err = appendBodyAttachments(*message, jobdata);
0177         if (!err.isNull())
0178         {
0179             qCCritical(KALARM_LOG) << "KAMail::send: Error compiling message:" << err;
0180             errmsgs = errors(err);
0181             return -1;
0182         }
0183 
0184         // Execute the send command
0185         FILE* fd = ::popen(command.toLocal8Bit().constData(), "w");
0186         if (!fd)
0187         {
0188             qCCritical(KALARM_LOG) << "KAMail::send: Unable to open a pipe to " << command;
0189             errmsgs = errors();
0190             return -1;
0191         }
0192         message->assemble();
0193         QByteArray encoded = message->encodedContent();
0194         fwrite(encoded.constData(), encoded.length(), 1, fd);
0195         pclose(fd);
0196 
0197         if (jobdata.allowNotify)
0198             notifyQueued(jobdata.event);
0199         return 1;
0200     }
0201     else if (Preferences::useAkonadi())
0202     {
0203         qCDebug(KALARM_LOG) << "KAMail::send: Sending via KDE";
0204         initHeaders(*message, jobdata);
0205         err = appendBodyAttachments(*message, jobdata);
0206         if (!err.isNull())
0207         {
0208             qCCritical(KALARM_LOG) << "KAMail::send: Error compiling message:" << err;
0209             errmsgs = errors(err);
0210             return -1;
0211         }
0212 
0213         if (!mAkonadiPlugin)
0214         {
0215             mAkonadiPlugin = Preferences::akonadiPlugin();
0216             connect(mAkonadiPlugin, &AkonadiPlugin::emailSent, instance(), &KAMail::akonadiEmailSent);
0217             connect(mAkonadiPlugin, &AkonadiPlugin::emailQueued, instance(), [](const KAEvent& e) { notifyQueued(e); });
0218         }
0219         const bool keepEmail = (Preferences::emailClient() == Preferences::kmail || Preferences::emailCopyToKMail());
0220         err = mAkonadiPlugin->sendMail(message, identity, extractEmailAndNormalize(jobdata.from), keepEmail, jobdata);
0221         if (!err.isEmpty())
0222         {
0223             errmsgs = errors(err);
0224             return -1;
0225         }
0226     }
0227     return 0;
0228 }
0229 
0230 /******************************************************************************
0231 * Called when sending an email via Akonadi is complete.
0232 */
0233 void KAMail::akonadiEmailSent(const JobData& jobdata, const QStringList& errmsgs, bool sendError)
0234 {
0235     QStringList messages = errmsgs;
0236     if (!errmsgs.isEmpty()  &&  sendError)
0237         messages = errors(errmsgs.at(0), SEND_ERROR);
0238     theApp()->emailSent(jobdata, messages);
0239 }
0240 
0241 /******************************************************************************
0242 * Append the body and attachments to the email text.
0243 * Reply = reason for error
0244 *       = empty string if successful.
0245 */
0246 QString KAMail::appendBodyAttachments(KMime::Message& message, JobData& data)
0247 {
0248     const QStringList attachments = data.event.emailAttachments();
0249     if (attachments.isEmpty())
0250     {
0251         // There are no attachments, so simply append the message body
0252         message.contentType()->setMimeType("text/plain");
0253         message.contentType()->setCharset("utf-8");
0254         message.fromUnicodeString(data.event.message());
0255         auto encodings = KMime::encodingsForData(message.body());
0256         encodings.removeAll(KMime::Headers::CE8Bit);  // not handled by KMime
0257         message.contentTransferEncoding()->setEncoding(encodings.at(0));
0258         message.assemble();
0259     }
0260     else
0261     {
0262         // There are attachments, so the message must be in MIME format
0263         message.contentType()->setMimeType("multipart/mixed");
0264         message.contentType()->setBoundary(KMime::multiPartBoundary());
0265 
0266         if (!data.event.message().isEmpty())
0267         {
0268             // There is a message body
0269             auto content = new KMime::Content();
0270             content->contentType()->setMimeType("text/plain");
0271             content->contentType()->setCharset("utf-8");
0272             content->fromUnicodeString(data.event.message());
0273             auto encodings = KMime::encodingsForData(content->body());
0274             encodings.removeAll(KMime::Headers::CE8Bit);  // not handled by KMime
0275             content->contentTransferEncoding()->setEncoding(encodings.at(0));
0276             content->assemble();
0277             message.appendContent(content);
0278         }
0279 
0280         // Append each attachment in turn
0281         for (const QString& att : attachments)
0282         {
0283             const QString attachment = QString::fromLatin1(att.toLocal8Bit());
0284             const QUrl url = QUrl::fromUserInput(attachment, QString(), QUrl::AssumeLocalFile);
0285             const QString attachError = xi18nc("@info", "Error attaching file: <filename>%1</filename>", attachment);
0286             QByteArray contents;
0287             bool atterror = false;
0288             if (!url.isLocalFile())
0289             {
0290                 auto statJob = KIO::stat(url, KIO::StatJob::SourceSide, KIO::StatDetail::StatDefaultDetails);
0291                 KJobWidgets::setWindow(statJob, MainWindow::mainMainWindow());
0292                 if (!statJob->exec())
0293                 {
0294                     qCCritical(KALARM_LOG) << "KAMail::appendBodyAttachments: Not found:" << attachment;
0295                     return xi18nc("@info", "Attachment not found: <filename>%1</filename>", attachment);
0296                 }
0297                 KFileItem fi(statJob->statResult(), url);
0298                 if (fi.isDir()  ||  !fi.isReadable())
0299                 {
0300                     qCCritical(KALARM_LOG) << "KAMail::appendBodyAttachments: Not file/not readable:" << attachment;
0301                     return attachError;
0302                 }
0303 
0304                 // Read the file contents
0305                 auto downloadJob = KIO::storedGet(url);
0306                 KJobWidgets::setWindow(downloadJob, MainWindow::mainMainWindow());
0307                 if (!downloadJob->exec())
0308                 {
0309                     qCCritical(KALARM_LOG) << "KAMail::appendBodyAttachments: Load failure:" << attachment;
0310                     return attachError;
0311                 }
0312                 contents = downloadJob->data();
0313                 if (static_cast<unsigned>(contents.size()) < fi.size())
0314                 {
0315                     qCDebug(KALARM_LOG) << "KAMail::appendBodyAttachments: Read error:" << attachment;
0316                     atterror = true;
0317                 }
0318             }
0319             else
0320             {
0321                 QFile f(url.toLocalFile());
0322                 if (!f.open(QIODevice::ReadOnly))
0323                 {
0324                     qCCritical(KALARM_LOG) << "KAMail::appendBodyAttachments: Load failure:" << attachment;
0325                     return attachError;
0326                 }
0327                 contents = f.readAll();
0328             }
0329 
0330             QByteArray coded = KCodecs::base64Encode(contents);
0331             auto content = new KMime::Content();
0332             content->setBody(coded + "\n\n");
0333 
0334             // Set the content type
0335             QMimeDatabase mimeDb;
0336             QString typeName = mimeDb.mimeTypeForUrl(url).name();
0337             auto ctype = new KMime::Headers::ContentType;
0338             ctype->fromUnicodeString(typeName, autoDetectCharset(typeName));
0339             ctype->setName(attachment, "local");
0340             content->setHeader(ctype);
0341 
0342             // Set the encoding
0343             auto cte = new KMime::Headers::ContentTransferEncoding;
0344             cte->setEncoding(KMime::Headers::CEbase64);
0345             cte->setDecoded(false);
0346             content->setHeader(cte);
0347             content->assemble();
0348             message.appendContent(content);
0349             if (atterror)
0350                 return attachError;
0351         }
0352         message.assemble();
0353     }
0354     return {};
0355 }
0356 
0357 /******************************************************************************
0358 * If any of the destination email addresses are non-local, display a
0359 * notification message saying that an email has been queued for sending.
0360 */
0361 void KAMail::notifyQueued(const KAEvent& event)
0362 {
0363     KMime::Types::Address addr;
0364     const QString localhost = QStringLiteral("localhost");
0365     const QString hostname  = QHostInfo::localHostName();
0366     const KCalendarCore::Person::List addresses = event.emailAddressees();
0367     for (const KCalendarCore::Person& address : addresses)
0368     {
0369         const QByteArray email = address.email().toLocal8Bit();
0370         const char* em = email.constData();
0371         if (!email.isEmpty()
0372         &&  HeaderParsing::parseAddress(em, em + email.length(), addr))
0373         {
0374             const QString domain = addr.mailboxList.at(0).addrSpec().domain;
0375             if (!domain.isEmpty()  &&  domain != localhost  &&  domain != hostname)
0376             {
0377                 KAMessageBox::information(MainWindow::mainMainWindow(), i18nc("@info", "An email has been queued to be sent"), QString(), Preferences::EMAIL_QUEUED_NOTIFY);
0378                 return;
0379             }
0380         }
0381     }
0382 }
0383 
0384 /******************************************************************************
0385 * Fetch the user's email address configured in KMail.
0386 */
0387 QString KAMail::controlCentreAddress()
0388 {
0389     KEMailSettings e;
0390     return e.getSetting(KEMailSettings::EmailAddress);
0391 }
0392 
0393 /******************************************************************************
0394 * Parse a list of email addresses, optionally containing display names,
0395 * entered by the user.
0396 * Reply = the invalid item if error, else empty string.
0397 */
0398 QString KAMail::convertAddresses(const QString& items, KCalendarCore::Person::List& list)
0399 {
0400     list.clear();
0401     QString invalidItem;
0402     const KMime::Types::Mailbox::List mailboxes = parseAddresses(items, invalidItem);
0403     if (!invalidItem.isEmpty())
0404         return invalidItem;
0405     for (const KMime::Types::Mailbox& mailbox : mailboxes)
0406     {
0407         const KCalendarCore::Person person(mailbox.name(), mailbox.addrSpec().asString());
0408         list += person;
0409     }
0410     return {};
0411 }
0412 
0413 /******************************************************************************
0414 * Check the validity of an email address.
0415 * Because internal email addresses don't have to abide by the usual internet
0416 * email address rules, only some basic checks are made.
0417 * Reply = 1 if alright, 0 if empty, -1 if error.
0418 */
0419 int KAMail::checkAddress(QString& address)
0420 {
0421     address = address.trimmed();
0422     // Check that there are no list separator characters present
0423     if (address.indexOf(QLatin1Char(',')) >= 0  ||  address.indexOf(QLatin1Char(';')) >= 0)
0424         return -1;
0425     const int n = address.length();
0426     if (!n)
0427         return 0;
0428     int start = 0;
0429     int end   = n - 1;
0430     if (address.at(end) == QLatin1Char('>'))
0431     {
0432         // The email address is in <...>
0433         if ((start = address.indexOf(QLatin1Char('<'))) < 0)
0434             return -1;
0435         ++start;
0436         --end;
0437     }
0438     const int i = address.indexOf(QLatin1Char('@'), start);
0439     if (i >= 0)
0440     {
0441         if (i == start  ||  i == end)          // check @ isn't the first or last character
0442 //        ||  address.indexOf(QLatin1Char('@'), i + 1) >= 0)    // check for multiple @ characters
0443             return -1;
0444     }
0445 /*    else
0446     {
0447         // Allow the @ character to be missing if it's a local user
0448         if (!getpwnam(address.mid(start, end - start + 1).toLocal8Bit()))
0449             return false;
0450     }
0451     for (int i = start;  i <= end;  ++i)
0452     {
0453         char ch = address.at(i).toLatin1();
0454         if (ch == '.'  ||  ch == '@'  ||  ch == '-'  ||  ch == '_'
0455         ||  (ch >= 'A' && ch <= 'Z')  ||  (ch >= 'a' && ch <= 'z')
0456         ||  (ch >= '0' && ch <= '9'))
0457             continue;
0458         return false;
0459     }*/
0460     return 1;
0461 }
0462 
0463 /******************************************************************************
0464 * Convert a comma or semicolon delimited list of attachments into a
0465 * QStringList. The items are checked for validity.
0466 * Reply = the invalid item if error, else empty string.
0467 */
0468 QString KAMail::convertAttachments(const QString& items, QStringList& list)
0469 {
0470     list.clear();
0471     int length = items.length();
0472     for (int next = 0;  next < length;  )
0473     {
0474         // Find the first delimiter character (, or ;)
0475         int i = items.indexOf(QLatin1Char(','), next);
0476         if (i < 0)
0477             i = items.length();
0478         int sc = items.indexOf(QLatin1Char(';'), next);
0479         if (sc < 0)
0480             sc = items.length();
0481         if (sc < i)
0482             i = sc;
0483         QString item = items.mid(next, i - next).trimmed();
0484         switch (checkAttachment(item))
0485         {
0486             case 1:   list += item;  break;
0487             case 0:   break;          // empty attachment name
0488             case -1:
0489             default:  return item;    // error
0490         }
0491         next = i + 1;
0492     }
0493     return {};
0494 }
0495 
0496 /******************************************************************************
0497 * Check for the existence of the attachment file.
0498 * If non-null, '*url' receives the QUrl of the attachment.
0499 * Reply = 1 if attachment exists
0500 *       = 0 if null name
0501 *       = -1 if doesn't exist.
0502 */
0503 int KAMail::checkAttachment(QString& attachment, QUrl* url)
0504 {
0505     attachment = attachment.trimmed();
0506     if (attachment.isEmpty())
0507     {
0508         if (url)
0509             *url = QUrl();
0510         return 0;
0511     }
0512     // Check that the file exists
0513     QUrl u = QUrl::fromUserInput(attachment, QString(), QUrl::AssumeLocalFile);
0514     u.setPath(QDir::cleanPath(u.path()));
0515     if (url)
0516         *url = u;
0517     return checkAttachment(u) ? 1 : -1;
0518 }
0519 
0520 /******************************************************************************
0521 * Check for the existence of the attachment file.
0522 */
0523 bool KAMail::checkAttachment(const QUrl& url)
0524 {
0525     auto statJob = KIO::stat(url);
0526     KJobWidgets::setWindow(statJob, MainWindow::mainMainWindow());
0527     if (!statJob->exec())
0528         return false;       // doesn't exist
0529     KFileItem fi(statJob->statResult(), url);
0530     if (fi.isDir()  ||  !fi.isReadable())
0531         return false;
0532     return true;
0533 }
0534 
0535 /******************************************************************************
0536 * Set the appropriate error messages for a given error string.
0537 */
0538 QStringList KAMail::errors(const QString& err, ErrType prefix)
0539 {
0540     QString error1;
0541     switch (prefix)
0542     {
0543         case SEND_FAIL:   error1 = i18nc("@info", "Failed to send email");  break;
0544         case SEND_ERROR:  error1 = i18nc("@info", "Error sending email");  break;
0545     }
0546     if (err.isEmpty())
0547         return QStringList(error1);
0548     return QStringList{QStringLiteral("%1:").arg(error1), err};
0549 }
0550 
0551 namespace
0552 {
0553 
0554 /******************************************************************************
0555 * Create the headers part of the email.
0556 */
0557 void initHeaders(KMime::Message& message, JobData& data)
0558 {
0559     auto date = new KMime::Headers::Date;
0560     date->setDateTime(KADateTime::currentDateTime(Preferences::timeSpec()).qDateTime());
0561     message.setHeader(date);
0562 
0563     auto from = new KMime::Headers::From;
0564     from->fromUnicodeString(data.from, autoDetectCharset(data.from));
0565     message.setHeader(from);
0566 
0567     auto to = new KMime::Headers::To;
0568     const KCalendarCore::Person::List toList = data.event.emailAddressees();
0569     for (const KCalendarCore::Person& who : toList)
0570         to->addAddress(who.email().toLatin1(), who.name());
0571     message.setHeader(to);
0572 
0573     if (!data.bcc.isEmpty())
0574     {
0575         auto bcc = new KMime::Headers::Bcc;
0576         bcc->fromUnicodeString(data.bcc, autoDetectCharset(data.bcc));
0577         message.setHeader(bcc);
0578     }
0579 
0580     auto subject = new KMime::Headers::Subject;
0581     const QString str = data.event.emailSubject();
0582     subject->fromUnicodeString(str, autoDetectCharset(str));
0583     message.setHeader(subject);
0584 
0585     auto agent = new KMime::Headers::UserAgent;
0586     agent->fromUnicodeString(KAboutData::applicationData().displayName() + QLatin1String("/" KALARM_VERSION), "us-ascii");
0587     message.setHeader(agent);
0588 
0589     auto id = new KMime::Headers::MessageID;
0590     id->generate(data.from.mid(data.from.indexOf(QLatin1Char('@')) + 1).toLatin1());
0591     message.setHeader(id);
0592 }
0593 
0594 /******************************************************************************
0595 * Extract the pure addresses from given email addresses.
0596 */
0597 QString extractEmailAndNormalize(const QString& emailAddress)
0598 {
0599     return KEmailAddress::extractEmailAddress(KEmailAddress::normalizeAddressesAndEncodeIdn(emailAddress));
0600 }
0601 
0602 //-----------------------------------------------------------------------------
0603 // Based on KMail KMMsgBase::autoDetectCharset().
0604 QByteArray autoDetectCharset(const QString& text)
0605 {
0606     for (QByteArray encoding : {"us-ascii", "iso-8859-1", "locale", "utf-8"})
0607     {
0608         if (encoding == "locale")
0609             encoding = QTextCodec::codecForLocale()->name().toLower();
0610         if (text.isEmpty())
0611             return encoding;
0612         if (encoding == "us-ascii")
0613         {
0614             if (KMime::isUsAscii(text))
0615                 return encoding;
0616         }
0617         else
0618         {
0619             const QTextCodec* codec = QTextCodec::codecForName(encoding.toLower());
0620             if (!codec)
0621                 qCDebug(KALARM_LOG) << "KAMail::autoDetectCharset: Something is wrong and I cannot get a codec. [" << encoding <<"]";
0622             else
0623             {
0624                  if (codec->canEncode(text))
0625                      return encoding;
0626             }
0627         }
0628     }
0629     return {};
0630 }
0631 
0632 /******************************************************************************
0633 * Parse a string containing multiple addresses, separated by comma or semicolon,
0634 * while retaining Unicode name parts.
0635 * Note that this only needs to parse strings input into KAlarm, so it only
0636 * needs to accept the common syntax for email addresses, not obsolete syntax.
0637 */
0638 KMime::Types::Mailbox::List parseAddresses(const QString& text, QString& invalidItem)
0639 {
0640     KMime::Types::Mailbox::List list;
0641     int state     = 0;
0642     int start     = 0;  // start of this item
0643     int endName   = 0;  // character after end of name
0644     int startAddr = 0;  // start of address
0645     int endAddr   = 0;  // character after end of address
0646     char lastch = '\0';
0647     bool ended    = false;   // found the end of the item
0648     for (int i = 0, count = text.length();  i <= count;  ++i)
0649     {
0650         if (i == count)
0651             ended = true;
0652         else
0653         {
0654             const char ch = text[i].toLatin1();
0655             switch (state)
0656             {
0657                 case 0:   // looking for start of item
0658                     if (ch == ' ' || ch == '\t')
0659                         continue;
0660                     start = i;
0661                     state = (ch == '"') ? 10 : 1;
0662                     break;
0663                 case 1:   // looking for start of address, or end of item
0664                     switch (ch)
0665                     {
0666                         case '<':
0667                             startAddr = i + 1;
0668                             state = 2;
0669                             break;
0670                         case ',':
0671                         case ';':
0672                             ended = true;
0673                             break;
0674                         case ' ':
0675                             break;
0676                         default:
0677                             endName = i + 1;
0678                             break;
0679                     }
0680                     break;
0681                 case 2:   // looking for '>' at end of address
0682                     if (ch == '>')
0683                     {
0684                         endAddr = i;
0685                         state = 3;
0686                     }
0687                     break;
0688                 case 3:   // looking for item separator
0689                     if (ch == ','  ||  ch == ';')
0690                         ended = true;
0691                     else if (ch != ' ')
0692                     {
0693                         invalidItem = text.mid(start);
0694                         return {};
0695                     }
0696                     break;
0697                 case 10:   // looking for closing quote
0698                     if (ch == '"'  &&  lastch != '\\')
0699                     {
0700                         ++start;   // remove opening quote from name
0701                         endName = i;
0702                         state = 11;
0703                     }
0704                     lastch = ch;
0705                     break;
0706                 case 11:   // looking for '<'
0707                     if (ch == '<')
0708                     {
0709                         startAddr = i + 1;
0710                         state = 2;
0711                     }
0712                     break;
0713             }
0714         }
0715         if (ended)
0716         {
0717             // Found the end of the item - add it to the list
0718             if (!startAddr)
0719             {
0720                 startAddr = start;
0721                 endAddr   = endName;
0722                 endName   = 0;
0723             }
0724             const QString addr = text.mid(startAddr, endAddr - startAddr);
0725             KMime::Types::Mailbox mbox;
0726             mbox.fromUnicodeString(addr);
0727             if (mbox.address().isEmpty())
0728             {
0729                 invalidItem = text.mid(start, endAddr - start);
0730                 return {};
0731             }
0732             if (endName)
0733             {
0734                 int len = endName - start;
0735                 QString name = text.mid(start, endName - start);
0736                 if (name.at(0) == QLatin1Char('"')  &&  name.at(len - 1) == QLatin1Char('"'))
0737                     name = name.mid(1, len - 2);
0738                 mbox.setName(name);
0739             }
0740             list.append(mbox);
0741 
0742             endName = startAddr = endAddr = 0;
0743             start = i + 1;
0744             state = 0;
0745             ended = false;
0746         }
0747     }
0748     return list;
0749 }
0750 
0751 } // namespace
0752 
0753 /*=============================================================================
0754 =  HeaderParsing :  modified and additional functions.
0755 =  The following functions are modified from, or additional to, those in
0756 =  libkmime kmime_header_parsing.cpp.
0757 =============================================================================*/
0758 
0759 namespace HeaderParsing
0760 {
0761 
0762 using namespace KMime;
0763 using namespace KMime::Types;
0764 using namespace KMime::HeaderParsing;
0765 
0766 /******************************************************************************
0767 * New function.
0768 * Allow a local user name to be specified as an email address.
0769 */
0770 bool parseUserName(const char* & scursor, const char* const send, QString& result, bool isCRLF)
0771 {
0772     QByteArray atom;
0773 
0774     if (scursor != send)
0775     {
0776         // first, eat any whitespace
0777         eatCFWS(scursor, send, isCRLF);
0778 
0779         char ch = *scursor++;
0780         switch (ch)
0781         {
0782             case '.': // dot
0783             case '@':
0784             case '"': // quoted-string
0785                 return false;
0786 
0787             default: // atom
0788                 scursor--; // re-set scursor to point to ch again
0789                 if (parseAtom(scursor, send, atom, false /* no 8bit */))
0790                 {
0791                     //TODO FIXME on windows
0792 #ifndef WIN32
0793                     if (getpwnam(atom.constData()))
0794                     {
0795                         result = QLatin1String(atom);
0796                         return true;
0797                     }
0798 #endif
0799                 }
0800                 return false; // parseAtom can only fail if the first char is non-atext.
0801         }
0802     }
0803     return false;
0804 }
0805 
0806 /******************************************************************************
0807 * Modified function.
0808 * Allow a local user name to be specified as an email address, and reinstate
0809 * the original scursor on error return.
0810 */
0811 bool parseAddress(const char* & scursor, const char* const send, Address& result, bool isCRLF)
0812 {
0813     // address       := mailbox / group
0814 
0815     eatCFWS(scursor, send, isCRLF);
0816     if (scursor == send)
0817         return false;
0818 
0819     // first try if it's a single mailbox:
0820     Mailbox maybeMailbox;
0821     const char * oldscursor = scursor;
0822     if (parseMailbox(scursor, send, maybeMailbox, isCRLF))
0823     {
0824         // yes, it is:
0825         result.displayName.clear();
0826         result.mailboxList.append(maybeMailbox);
0827         return true;
0828     }
0829     scursor = oldscursor;
0830 
0831     // KAlarm: Allow a local user name to be specified
0832     // no, it's not a single mailbox. Try if it's a local user name:
0833     QString maybeUserName;
0834     if (parseUserName(scursor, send, maybeUserName, isCRLF))
0835     {
0836         // yes, it is:
0837         maybeMailbox.setName(QString());
0838         AddrSpec addrSpec;
0839         addrSpec.localPart = maybeUserName;
0840         addrSpec.domain.clear();
0841         maybeMailbox.setAddress(addrSpec);
0842         result.displayName.clear();
0843         result.mailboxList.append(maybeMailbox);
0844         return true;
0845     }
0846     scursor = oldscursor;
0847 
0848     Address maybeAddress;
0849 
0850     // no, it's not a single mailbox. Try if it's a group:
0851     if (!parseGroup(scursor, send, maybeAddress, isCRLF))
0852     {
0853         scursor = oldscursor;   // KAlarm: reinstate original scursor on error return
0854         return false;
0855     }
0856 
0857     result = maybeAddress;
0858     return true;
0859 }
0860 
0861 } // namespace HeaderParsing
0862 
0863 #include "moc_kamail.cpp"
0864 
0865 // vim: et sw=4: