Move unittestenv to correct location
[kdepim.git] / kalarm / kamail.cpp
blobf63f2bda6bb8f1b0d5039667134ba56dc6146b62
1 /*
2 * kamail.cpp - email functions
3 * Program: kalarm
4 * Copyright © 2002-2015 by David Jarvie <djarvie@kde.org>
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21 #include "kalarm.h" //krazy:exclude=includes (kalarm.h must be first)
22 #include "kamail.h"
24 #include "functions.h"
25 #include "kalarmapp.h"
26 #include "mainwindow.h"
27 #include "messagebox.h"
28 #include "preferences.h"
30 #include <kalarmcal/identities.h>
31 #include <KIdentityManagement/kidentitymanagement/identitymanager.h>
32 #include <KIdentityManagement/kidentitymanagement/identity.h>
33 #include <MailTransport/mailtransport/transportmanager.h>
34 #include <MailTransport/mailtransport/transport.h>
35 #include <MailTransport/mailtransport/messagequeuejob.h>
36 #include <KCalCore/Person>
37 #include <kmime/kmime_header_parsing.h>
38 #include <kmime/kmime_headers.h>
39 #include <kmime/kmime_message.h>
41 #include <KEmailAddress>
42 #include <KAboutData>
43 #include <KLocale>
44 #include <KLocalizedString>
45 #include <kfileitem.h>
46 #include <KIO/StatJob>
47 #include <KJobWidgets>
48 #include <kemailsettings.h>
49 #include <kcodecs.h>
50 #include <kcharsets.h>
51 #include <kshell.h>
53 #include <QUrl>
54 #include <QFile>
55 #include <QHostInfo>
56 #include <QList>
57 #include <QByteArray>
58 #include <QTextCodec>
59 #include <QStandardPaths>
60 #include <QtDBus/QtDBus>
61 #include "kalarm_debug.h"
63 #include <pwd.h>
65 #include "kmailinterface.h"
67 static const QLatin1String KMAIL_DBUS_SERVICE("org.kde.kmail");
68 //static const QLatin1String KMAIL_DBUS_PATH("/KMail");
70 namespace HeaderParsing
72 bool parseAddress( const char* & scursor, const char * const send,
73 KMime::Types::Address & result, bool isCRLF=false );
76 static void initHeaders(KMime::Message&, KAMail::JobData&);
77 static KMime::Types::Mailbox::List parseAddresses(const QString& text, QString& invalidItem);
78 static QString extractEmailAndNormalize(const QString& emailAddress);
79 static QStringList extractEmailsAndNormalize(const QString& emailAddresses);
80 static QByteArray autoDetectCharset(const QString& text);
81 static const QTextCodec* codecForName(const QByteArray& str);
83 QString KAMail::i18n_NeedFromEmailAddress()
84 { return i18nc("@info", "A 'From' email address must be configured in order to execute email alarms."); }
86 QString KAMail::i18n_sent_mail()
87 { return i18nc("@info KMail folder name: this should be translated the same as in kmail", "sent-mail"); }
89 KAMail* KAMail::mInstance = Q_NULLPTR; // used only to enable signals/slots to work
90 QQueue<MailTransport::MessageQueueJob*> KAMail::mJobs;
91 QQueue<KAMail::JobData> KAMail::mJobData;
93 KAMail* KAMail::instance()
95 if (!mInstance)
96 mInstance = new KAMail();
97 return mInstance;
100 /******************************************************************************
101 * Send the email message specified in an event.
102 * Reply = 1 if the message was sent - 'errmsgs' may contain copy error messages.
103 * = 0 if the message is queued for sending.
104 * = -1 if the message was not sent - 'errmsgs' contains the error messages.
106 int KAMail::send(JobData& jobdata, QStringList& errmsgs)
108 QString err;
109 KIdentityManagement::Identity identity;
110 if (!jobdata.event.emailFromId())
111 jobdata.from = Preferences::emailAddress();
112 else
114 identity = Identities::identityManager()->identityForUoid(jobdata.event.emailFromId());
115 if (identity.isNull())
117 qCCritical(KALARM_LOG) << "Identity" << jobdata.event.emailFromId() << "not found";
118 errmsgs = errors(xi18nc("@info", "Invalid 'From' email address.<nl/>Email identity <resource>%1</resource> not found", jobdata.event.emailFromId()));
119 return -1;
121 if (identity.primaryEmailAddress().isEmpty())
123 qCCritical(KALARM_LOG) << "Identity" << identity.identityName() << "uoid" << identity.uoid() << ": no email address";
124 errmsgs = errors(xi18nc("@info", "Invalid 'From' email address.<nl/>Email identity <resource>%1</resource> has no email address", identity.identityName()));
125 return -1;
127 jobdata.from = identity.fullEmailAddr();
129 if (jobdata.from.isEmpty())
131 switch (Preferences::emailFrom())
133 case Preferences::MAIL_FROM_KMAIL:
134 errmsgs = errors(xi18nc("@info", "<para>No 'From' email address is configured (no default email identity found)</para>"
135 "<para>Please set it in <application>KMail</application> or in the <application>KAlarm</application> Configuration dialog.</para>"));
136 break;
137 case Preferences::MAIL_FROM_SYS_SETTINGS:
138 errmsgs = errors(xi18nc("@info", "<para>No 'From' email address is configured.</para>"
139 "<para>Please set it in the KDE System Settings or in the <application>KAlarm</application> Configuration dialog.</para>"));
140 break;
141 case Preferences::MAIL_FROM_ADDR:
142 default:
143 errmsgs = errors(xi18nc("@info", "<para>No 'From' email address is configured.</para>"
144 "<para>Please set it in the <application>KAlarm</application> Configuration dialog.</para>"));
145 break;
147 return -1;
149 jobdata.bcc = (jobdata.event.emailBcc() ? Preferences::emailBccAddress() : QString());
150 qCDebug(KALARM_LOG) << "To:" << jobdata.event.emailAddresses(QStringLiteral(","))
151 << endl << "Subject:" << jobdata.event.emailSubject();
153 KMime::Message::Ptr message = KMime::Message::Ptr(new KMime::Message);
155 MailTransport::TransportManager* manager = MailTransport::TransportManager::self();
156 MailTransport::Transport* transport = Q_NULLPTR;
157 if (Preferences::emailClient() == Preferences::sendmail)
159 qCDebug(KALARM_LOG) << "Sending via sendmail";
160 QStringList paths;
161 paths << QStringLiteral("/sbin") << QStringLiteral("/usr/sbin") << QStringLiteral("/usr/lib");
162 QString command = QStandardPaths::findExecutable(QStringLiteral("sendmail"), paths);
163 if (!command.isNull())
165 command += QStringLiteral(" -f ");
166 command += extractEmailAndNormalize(jobdata.from);
167 command += QStringLiteral(" -oi -t ");
168 initHeaders(*message, jobdata);
170 else
172 command = QStandardPaths::findExecutable(QStringLiteral("mail"), paths);
173 if (command.isNull())
175 qCCritical(KALARM_LOG) << "sendmail not found";
176 errmsgs = errors(xi18nc("@info", "<command>%1</command> not found", QStringLiteral("sendmail"))); // give up
177 return -1;
180 command += QStringLiteral(" -s ");
181 command += KShell::quoteArg(jobdata.event.emailSubject());
183 if (!jobdata.bcc.isEmpty())
185 command += QStringLiteral(" -b ");
186 command += extractEmailAndNormalize(jobdata.bcc);
189 command += QLatin1Char(' ');
190 command += jobdata.event.emailPureAddresses(QStringLiteral(" ")); // locally provided, okay
192 // Add the body and attachments to the message.
193 // (Sendmail requires attachments to have already been included in the message.)
194 err = appendBodyAttachments(*message, jobdata);
195 if (!err.isNull())
197 qCCritical(KALARM_LOG) << "Error compiling message:" << err;
198 errmsgs = errors(err);
199 return -1;
202 // Execute the send command
203 FILE* fd = ::popen(command.toLocal8Bit(), "w");
204 if (!fd)
206 qCCritical(KALARM_LOG) << "Unable to open a pipe to " << command;
207 errmsgs = errors();
208 return -1;
210 message->assemble();
211 QByteArray encoded = message->encodedContent();
212 fwrite(encoded, encoded.length(), 1, fd);
213 pclose(fd);
215 #ifdef KMAIL_SUPPORTED
216 if (Preferences::emailCopyToKMail())
218 // Create a copy of the sent email in KMail's 'sent-mail' folder,
219 // or if there was a send error, in KMail's 'outbox' folder.
220 err = addToKMailFolder(jobdata, "sent-mail", true);
221 if (!err.isNull())
222 errmsgs += errors(err, COPY_ERROR); // not a fatal error - continue
224 #endif
226 if (jobdata.allowNotify)
227 notifyQueued(jobdata.event);
228 return 1;
230 else
232 qCDebug(KALARM_LOG) << "Sending via KDE";
233 const int transportId = identity.transport().isEmpty() ? -1 : identity.transport().toInt();
234 transport = manager->transportById(transportId, true);
235 if (!transport)
237 qCCritical(KALARM_LOG) << "No mail transport found for identity" << identity.identityName() << "uoid" << identity.uoid();
238 errmsgs = errors(xi18nc("@info", "No mail transport configured for email identity <resource>%1</resource>", identity.identityName()));
239 return -1;
241 qCDebug(KALARM_LOG) << "Using transport" << transport->name() << ", id=" << transport->id();
243 initHeaders(*message, jobdata);
244 err = appendBodyAttachments(*message, jobdata);
245 if (!err.isNull())
247 qCCritical(KALARM_LOG) << "Error compiling message:" << err;
248 errmsgs = errors(err);
249 return -1;
252 MailTransport::MessageQueueJob* mailjob = new MailTransport::MessageQueueJob(qApp);
253 mailjob->setMessage(message);
254 mailjob->transportAttribute().setTransportId(transport->id());
255 // MessageQueueJob email addresses must be pure, i.e. without display name. Note
256 // that display names are included in the actual headers set up by initHeaders().
257 mailjob->addressAttribute().setFrom(extractEmailAndNormalize(jobdata.from));
258 mailjob->addressAttribute().setTo(extractEmailsAndNormalize(jobdata.event.emailAddresses(QStringLiteral(","))));
259 if (!jobdata.bcc.isEmpty())
260 mailjob->addressAttribute().setBcc(extractEmailsAndNormalize(jobdata.bcc));
261 MailTransport::SentBehaviourAttribute::SentBehaviour sentAction =
262 (Preferences::emailClient() == Preferences::kmail || Preferences::emailCopyToKMail())
263 ? MailTransport::SentBehaviourAttribute::MoveToDefaultSentCollection : MailTransport::SentBehaviourAttribute::Delete;
264 mailjob->sentBehaviourAttribute().setSentBehaviour(sentAction);
265 mJobs.enqueue(mailjob);
266 mJobData.enqueue(jobdata);
267 if (mJobs.count() == 1)
269 // There are no jobs already active or queued, so send now
270 connect(mailjob, &KJob::result, instance(), &KAMail::slotEmailSent);
271 mailjob->start();
274 return 0;
277 /******************************************************************************
278 * Called when sending an email is complete.
280 void KAMail::slotEmailSent(KJob* job)
282 bool copyerr = false;
283 QStringList errmsgs;
284 if (job->error())
286 qCCritical(KALARM_LOG) << "Failed:" << job->errorString();
287 errmsgs = errors(job->errorString(), SEND_ERROR);
289 JobData jobdata;
290 if (mJobs.isEmpty() || mJobData.isEmpty() || job != mJobs.head())
292 // The queue has been corrupted, so we can't locate the job's data
293 qCCritical(KALARM_LOG) << "Wrong job at head of queue: wiping queue";
294 mJobs.clear();
295 mJobData.clear();
296 if (!errmsgs.isEmpty())
297 theApp()->emailSent(jobdata, errmsgs);
298 errmsgs.clear();
299 errmsgs += i18nc("@info", "Emails may not have been sent");
300 errmsgs += i18nc("@info", "Program error");
301 theApp()->emailSent(jobdata, errmsgs);
302 return;
304 mJobs.dequeue();
305 jobdata = mJobData.dequeue();
306 if (jobdata.allowNotify)
307 notifyQueued(jobdata.event);
308 theApp()->emailSent(jobdata, errmsgs, copyerr);
309 if (!mJobs.isEmpty())
311 // Send the next queued email
312 connect(mJobs.head(), &KJob::result, instance(), &KAMail::slotEmailSent);
313 mJobs.head()->start();
317 /******************************************************************************
318 * Create the headers part of the email.
320 void initHeaders(KMime::Message& message, KAMail::JobData& data)
322 KMime::Headers::Date* date = new KMime::Headers::Date;
323 //QT5 port it
324 date->setDateTime(QDateTime::currentDateTime(/*Preferences::timeZone()*/));
325 message.setHeader(date);
327 KMime::Headers::From* from = new KMime::Headers::From;
328 from->fromUnicodeString(data.from, autoDetectCharset(data.from));
329 message.setHeader(from);
331 KMime::Headers::To* to = new KMime::Headers::To;
332 KCalCore::Person::List toList = data.event.emailAddressees();
333 for (int i = 0, count = toList.count(); i < count; ++i)
334 to->addAddress(toList[i]->email().toLatin1(), toList[i]->name());
335 message.setHeader(to);
337 if (!data.bcc.isEmpty())
339 KMime::Headers::Bcc* bcc = new KMime::Headers::Bcc;
340 bcc->fromUnicodeString(data.bcc, autoDetectCharset(data.bcc));
341 message.setHeader(bcc);
344 KMime::Headers::Subject* subject = new KMime::Headers::Subject;
345 QString str = data.event.emailSubject();
346 subject->fromUnicodeString(str, autoDetectCharset(str));
347 message.setHeader(subject);
349 KMime::Headers::UserAgent* agent = new KMime::Headers::UserAgent;
350 agent->fromUnicodeString(KAboutData::applicationData().displayName() + QLatin1String("/" KALARM_VERSION), "us-ascii");
351 message.setHeader(agent);
353 KMime::Headers::MessageID* id = new KMime::Headers::MessageID;
354 id->generate(data.from.mid(data.from.indexOf(QLatin1Char('@')) + 1).toLatin1());
355 message.setHeader(id);
358 /******************************************************************************
359 * Append the body and attachments to the email text.
360 * Reply = reason for error
361 * = empty string if successful.
363 QString KAMail::appendBodyAttachments(KMime::Message& message, JobData& data)
365 QStringList attachments = data.event.emailAttachments();
366 if (!attachments.count())
368 // There are no attachments, so simply append the message body
369 message.contentType()->setMimeType("text/plain");
370 message.contentType()->setCharset("utf-8");
371 message.fromUnicodeString(data.event.message());
372 auto encodings = KMime::encodingsForData(message.body());
373 encodings.removeAll(KMime::Headers::CE8Bit); // not handled by KMime
374 message.contentTransferEncoding()->setEncoding(encodings[0]);
375 message.assemble();
377 else
379 // There are attachments, so the message must be in MIME format
380 message.contentType()->setMimeType("multipart/mixed");
381 message.contentType()->setBoundary(KMime::multiPartBoundary());
383 if (!data.event.message().isEmpty())
385 // There is a message body
386 KMime::Content* content = new KMime::Content();
387 content->contentType()->setMimeType("text/plain");
388 content->contentType()->setCharset("utf-8");
389 content->fromUnicodeString(data.event.message());
390 auto encodings = KMime::encodingsForData(content->body());
391 encodings.removeAll(KMime::Headers::CE8Bit); // not handled by KMime
392 content->contentTransferEncoding()->setEncoding(encodings[0]);
393 content->assemble();
394 message.addContent(content);
397 // Append each attachment in turn
398 for (QStringList::Iterator at = attachments.begin(); at != attachments.end(); ++at)
400 QString attachment = QString::fromLatin1((*at).toLocal8Bit());
401 QUrl url = QUrl::fromUserInput(attachment, QString(), QUrl::AssumeLocalFile);
402 QString attachError = xi18nc("@info", "Error attaching file: <filename>%1</filename>", attachment);
403 QByteArray contents;
404 bool atterror = false;
405 if (!url.isLocalFile())
407 KIO::UDSEntry uds;
408 auto statJob = KIO::stat(url, KIO::StatJob::SourceSide, 2);
409 KJobWidgets::setWindow(statJob, MainWindow::mainMainWindow());
410 if (!statJob->exec())
412 qCCritical(KALARM_LOG) << "Not found:" << attachment;
413 return xi18nc("@info", "Attachment not found: <filename>%1</filename>", attachment);
415 KFileItem fi(statJob->statResult(), url);
416 if (fi.isDir() || !fi.isReadable())
418 qCCritical(KALARM_LOG) << "Not file/not readable:" << attachment;
419 return attachError;
422 // Read the file contents
423 auto downloadJob = KIO::storedGet(url.url());
424 KJobWidgets::setWindow(downloadJob, MainWindow::mainMainWindow());
425 if (!downloadJob->exec())
427 qCCritical(KALARM_LOG) << "Load failure:" << attachment;
428 return attachError;
430 contents = downloadJob->data();
431 if (static_cast<unsigned>(contents.size()) < fi.size())
433 qCDebug(KALARM_LOG) << "Read error:" << attachment;
434 atterror = true;
437 else
439 QFile f(url.toLocalFile());
440 if (!f.open(QIODevice::ReadOnly))
442 qCCritical(KALARM_LOG) << "Load failure:" << attachment;
443 return attachError;
445 contents = f.readAll();
448 QByteArray coded = KCodecs::base64Encode(contents, true);
449 KMime::Content* content = new KMime::Content();
450 content->setBody(coded + "\n\n");
452 // Set the content type
453 QMimeDatabase mimeDb;
454 QString typeName = mimeDb.mimeTypeForUrl(url).name();
455 KMime::Headers::ContentType* ctype = new KMime::Headers::ContentType;
456 ctype->fromUnicodeString(typeName, autoDetectCharset(typeName));
457 ctype->setName(attachment, "local");
458 content->setHeader(ctype);
460 // Set the encoding
461 KMime::Headers::ContentTransferEncoding* cte = new KMime::Headers::ContentTransferEncoding;
462 cte->setEncoding(KMime::Headers::CEbase64);
463 cte->setDecoded(false);
464 content->setHeader(cte);
465 content->assemble();
466 message.addContent(content);
467 if (atterror)
468 return attachError;
470 message.assemble();
472 return QString();
475 /******************************************************************************
476 * If any of the destination email addresses are non-local, display a
477 * notification message saying that an email has been queued for sending.
479 void KAMail::notifyQueued(const KAEvent& event)
481 KMime::Types::Address addr;
482 const QString localhost = QStringLiteral("localhost");
483 const QString hostname = QHostInfo::localHostName();
484 KCalCore::Person::List addresses = event.emailAddressees();
485 for (int i = 0, end = addresses.count(); i < end; ++i)
487 QByteArray email = addresses[i]->email().toLocal8Bit();
488 const char* em = email;
489 if (!email.isEmpty()
490 && HeaderParsing::parseAddress(em, em + email.length(), addr))
492 QString domain = addr.mailboxList.at(0).addrSpec().domain;
493 if (!domain.isEmpty() && domain != localhost && domain != hostname)
495 KAMessageBox::information(MainWindow::mainMainWindow(), i18nc("@info", "An email has been queued to be sent"), QString(), Preferences::EMAIL_QUEUED_NOTIFY);
496 return;
502 /******************************************************************************
503 * Fetch the user's email address configured in the KDE System Settings.
505 QString KAMail::controlCentreAddress()
507 KEMailSettings e;
508 return e.getSetting(KEMailSettings::EmailAddress);
511 /******************************************************************************
512 * Parse a list of email addresses, optionally containing display names,
513 * entered by the user.
514 * Reply = the invalid item if error, else empty string.
516 QString KAMail::convertAddresses(const QString& items, KCalCore::Person::List& list)
518 list.clear();
519 QString invalidItem;
520 const KMime::Types::Mailbox::List mailboxes = parseAddresses(items, invalidItem);
521 if (!invalidItem.isEmpty())
522 return invalidItem;
523 for (int i = 0, count = mailboxes.count(); i < count; ++i)
525 KCalCore::Person::Ptr person(new KCalCore::Person(mailboxes[i].name(), mailboxes[i].addrSpec().asString()));
526 list += person;
528 return QString();
531 /******************************************************************************
532 * Check the validity of an email address.
533 * Because internal email addresses don't have to abide by the usual internet
534 * email address rules, only some basic checks are made.
535 * Reply = 1 if alright, 0 if empty, -1 if error.
537 int KAMail::checkAddress(QString& address)
539 address = address.trimmed();
540 // Check that there are no list separator characters present
541 if (address.indexOf(QLatin1Char(',')) >= 0 || address.indexOf(QLatin1Char(';')) >= 0)
542 return -1;
543 int n = address.length();
544 if (!n)
545 return 0;
546 int start = 0;
547 int end = n - 1;
548 if (address[end] == QLatin1Char('>'))
550 // The email address is in <...>
551 if ((start = address.indexOf(QLatin1Char('<'))) < 0)
552 return -1;
553 ++start;
554 --end;
556 int i = address.indexOf(QLatin1Char('@'), start);
557 if (i >= 0)
559 if (i == start || i == end) // check @ isn't the first or last character
560 // || address.indexOf(QLatin1Char('@'), i + 1) >= 0) // check for multiple @ characters
561 return -1;
563 /* else
565 // Allow the @ character to be missing if it's a local user
566 if (!getpwnam(address.mid(start, end - start + 1).toLocal8Bit()))
567 return false;
569 for (int i = start; i <= end; ++i)
571 char ch = address[i].toLatin1();
572 if (ch == '.' || ch == '@' || ch == '-' || ch == '_'
573 || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')
574 || (ch >= '0' && ch <= '9'))
575 continue;
576 return false;
578 return 1;
581 /******************************************************************************
582 * Convert a comma or semicolon delimited list of attachments into a
583 * QStringList. The items are checked for validity.
584 * Reply = the invalid item if error, else empty string.
586 QString KAMail::convertAttachments(const QString& items, QStringList& list)
588 list.clear();
589 int length = items.length();
590 for (int next = 0; next < length; )
592 // Find the first delimiter character (, or ;)
593 int i = items.indexOf(QLatin1Char(','), next);
594 if (i < 0)
595 i = items.length();
596 int sc = items.indexOf(QLatin1Char(';'), next);
597 if (sc < 0)
598 sc = items.length();
599 if (sc < i)
600 i = sc;
601 QString item = items.mid(next, i - next).trimmed();
602 switch (checkAttachment(item))
604 case 1: list += item; break;
605 case 0: break; // empty attachment name
606 case -1:
607 default: return item; // error
609 next = i + 1;
611 return QString();
614 /******************************************************************************
615 * Check for the existence of the attachment file.
616 * If non-null, '*url' receives the QUrl of the attachment.
617 * Reply = 1 if attachment exists
618 * = 0 if null name
619 * = -1 if doesn't exist.
621 int KAMail::checkAttachment(QString& attachment, QUrl* url)
623 attachment = attachment.trimmed();
624 if (attachment.isEmpty())
626 if (url)
627 *url = QUrl();
628 return 0;
630 // Check that the file exists
631 QUrl u = QUrl::fromUserInput(attachment, QString(), QUrl::AssumeLocalFile);
632 u.setPath(QDir::cleanPath(u.path()));
633 if (url)
634 *url = u;
635 return checkAttachment(u) ? 1 : -1;
638 /******************************************************************************
639 * Check for the existence of the attachment file.
641 bool KAMail::checkAttachment(const QUrl& url)
643 auto statJob = KIO::stat(url);
644 KJobWidgets::setWindow(statJob, MainWindow::mainMainWindow());
645 if (!statJob->exec())
646 return false; // doesn't exist
647 KFileItem fi(statJob->statResult(), url);
648 if (fi.isDir() || !fi.isReadable())
649 return false;
650 return true;
653 /******************************************************************************
654 * Set the appropriate error messages for a given error string.
656 QStringList KAMail::errors(const QString& err, ErrType prefix)
658 QString error1;
659 switch (prefix)
661 case SEND_FAIL: error1 = i18nc("@info", "Failed to send email"); break;
662 case SEND_ERROR: error1 = i18nc("@info", "Error sending email"); break;
663 #ifdef KMAIL_SUPPORTED
664 case COPY_ERROR: error1 = i18nc("@info", "Error copying sent email to <application>KMail</application> <resource>%1</resource> folder", i18n_sent_mail()); break;
665 #endif
667 if (err.isEmpty())
668 return QStringList(error1);
669 QStringList errs(QStringLiteral("%1:").arg(error1));
670 errs += err;
671 return errs;
674 /******************************************************************************
675 * Get the body of an email from KMail, given its serial number.
677 QString KAMail::getMailBody(quint32 serialNumber)
679 //TODO: Need to use Akonadi instead
680 QList<QVariant> args;
681 args << serialNumber << (int)0;
682 QDBusInterface iface(KMAIL_DBUS_SERVICE, QString(), QStringLiteral("KMailIface"));
683 QDBusReply<QString> reply = iface.callWithArgumentList(QDBus::Block, QStringLiteral("getDecodedBodyPart"), args);
684 if (!reply.isValid())
686 qCCritical(KALARM_LOG) << "D-Bus call failed:" << reply.error().message();
687 return QString();
689 return reply.value();
692 /******************************************************************************
693 * Extract the pure addresses from given email addresses.
695 QString extractEmailAndNormalize(const QString& emailAddress)
697 return KEmailAddress::extractEmailAddress(KEmailAddress::normalizeAddressesAndEncodeIdn(emailAddress));
700 QStringList extractEmailsAndNormalize(const QString& emailAddresses)
702 const QStringList splitEmails(KEmailAddress::splitAddressList(emailAddresses));
703 QStringList normalizedEmail;
704 Q_FOREACH(const QString& email, splitEmails)
706 normalizedEmail << KEmailAddress::extractEmailAddress(KEmailAddress::normalizeAddressesAndEncodeIdn(email));
708 return normalizedEmail;
711 //-----------------------------------------------------------------------------
712 // Based on KMail KMMsgBase::autoDetectCharset().
713 QByteArray autoDetectCharset(const QString& text)
715 static QList<QByteArray> charsets;
716 if (charsets.isEmpty())
717 charsets << "us-ascii" << "iso-8859-1" << "locale" << "utf-8";
719 for (int i = 0, count = charsets.count(); i < count; ++i)
721 QByteArray encoding = charsets[i];
722 if (encoding == "locale")
724 encoding = QTextCodec::codecForName(KLocale::global()->encoding())->name();
725 encoding = encoding.toLower();
727 if (text.isEmpty())
728 return encoding;
729 if (encoding == "us-ascii")
731 if (KMime::isUsAscii(text))
732 return encoding;
734 else
736 const QTextCodec *codec = codecForName(encoding);
737 if (!codec)
738 qCDebug(KALARM_LOG) <<"Auto-Charset: Something is wrong and I cannot get a codec. [" << encoding <<"]";
739 else
741 if (codec->canEncode(text))
742 return encoding;
746 return QByteArray();
749 //-----------------------------------------------------------------------------
750 // Based on KMail KMMsgBase::codecForName().
751 const QTextCodec* codecForName(const QByteArray& str)
753 if (str.isEmpty())
754 return Q_NULLPTR;
755 QByteArray codec = str.toLower();
756 return KCharsets::charsets()->codecForName(QLatin1String(codec));
759 /******************************************************************************
760 * Parse a string containing multiple addresses, separated by comma or semicolon,
761 * while retaining Unicode name parts.
762 * Note that this only needs to parse strings input into KAlarm, so it only
763 * needs to accept the common syntax for email addresses, not obsolete syntax.
765 KMime::Types::Mailbox::List parseAddresses(const QString& text, QString& invalidItem)
767 KMime::Types::Mailbox::List list;
768 int state = 0;
769 int start = 0; // start of this item
770 int endName = 0; // character after end of name
771 int startAddr = 0; // start of address
772 int endAddr = 0; // character after end of address
773 char lastch = '\0';
774 bool ended = false; // found the end of the item
775 for (int i = 0, count = text.length(); i <= count; ++i)
777 if (i == count)
778 ended = true;
779 else
781 char ch = text[i].toLatin1();
782 switch (state)
784 case 0: // looking for start of item
785 if (ch == ' ' || ch == '\t')
786 continue;
787 start = i;
788 state = (ch == '"') ? 10 : 1;
789 break;
790 case 1: // looking for start of address, or end of item
791 switch (ch)
793 case '<':
794 startAddr = i + 1;
795 state = 2;
796 break;
797 case ',':
798 case ';':
799 ended = true;
800 break;
801 case ' ':
802 break;
803 default:
804 endName = i + 1;
805 break;
807 break;
808 case 2: // looking for '>' at end of address
809 if (ch == '>')
811 endAddr = i;
812 state = 3;
814 break;
815 case 3: // looking for item separator
816 if (ch == ',' || ch == ';')
817 ended = true;
818 else if (ch != ' ')
820 invalidItem = text.mid(start);
821 return KMime::Types::Mailbox::List();
823 break;
824 case 10: // looking for closing quote
825 if (ch == '"' && lastch != '\\')
827 ++start; // remove opening quote from name
828 endName = i;
829 state = 11;
831 lastch = ch;
832 break;
833 case 11: // looking for '<'
834 if (ch == '<')
836 startAddr = i + 1;
837 state = 2;
839 break;
842 if (ended)
844 // Found the end of the item - add it to the list
845 if (!startAddr)
847 startAddr = start;
848 endAddr = endName;
849 endName = 0;
851 QString addr = text.mid(startAddr, endAddr - startAddr);
852 KMime::Types::Mailbox mbox;
853 mbox.fromUnicodeString(addr);
854 if (mbox.address().isEmpty())
856 invalidItem = text.mid(start, endAddr - start);
857 return KMime::Types::Mailbox::List();
859 if (endName)
861 int len = endName - start;
862 QString name = text.mid(start, endName - start);
863 if (name[0] == QLatin1Char('"') && name[len - 1] == QLatin1Char('"'))
864 name = name.mid(1, len - 2);
865 mbox.setName(name);
867 list.append(mbox);
869 endName = startAddr = endAddr = 0;
870 start = i + 1;
871 state = 0;
872 ended = false;
875 return list;
878 /*=============================================================================
879 = HeaderParsing : modified and additional functions.
880 = The following functions are modified from, or additional to, those in
881 = libkmime kmime_header_parsing.cpp.
882 =============================================================================*/
884 namespace HeaderParsing
887 using namespace KMime;
888 using namespace KMime::Types;
889 using namespace KMime::HeaderParsing;
891 /******************************************************************************
892 * New function.
893 * Allow a local user name to be specified as an email address.
895 bool parseUserName( const char* & scursor, const char * const send,
896 QString & result, bool isCRLF ) {
898 QString maybeLocalPart;
899 QString tmp;
901 if ( scursor != send ) {
902 // first, eat any whitespace
903 eatCFWS( scursor, send, isCRLF );
905 char ch = *scursor++;
906 switch ( ch ) {
907 case '.': // dot
908 case '@':
909 case '"': // quoted-string
910 return false;
912 default: // atom
913 scursor--; // re-set scursor to point to ch again
914 tmp.clear();
915 if ( parseAtom( scursor, send, result, false /* no 8bit */ ) ) {
916 if (getpwnam(result.toLocal8Bit()))
917 return true;
919 return false; // parseAtom can only fail if the first char is non-atext.
922 return false;
925 /******************************************************************************
926 * Modified function.
927 * Allow a local user name to be specified as an email address, and reinstate
928 * the original scursor on error return.
930 bool parseAddress( const char* & scursor, const char * const send,
931 Address & result, bool isCRLF ) {
932 // address := mailbox / group
934 eatCFWS( scursor, send, isCRLF );
935 if ( scursor == send )
936 return false;
938 // first try if it's a single mailbox:
939 Mailbox maybeMailbox;
940 const char * oldscursor = scursor;
941 if ( parseMailbox( scursor, send, maybeMailbox, isCRLF ) ) {
942 // yes, it is:
943 result.displayName.clear();
944 result.mailboxList.append( maybeMailbox );
945 return true;
947 scursor = oldscursor;
949 // KAlarm: Allow a local user name to be specified
950 // no, it's not a single mailbox. Try if it's a local user name:
951 QString maybeUserName;
952 if ( parseUserName( scursor, send, maybeUserName, isCRLF ) ) {
953 // yes, it is:
954 maybeMailbox.setName( QString() );
955 AddrSpec addrSpec;
956 addrSpec.localPart = maybeUserName;
957 addrSpec.domain.clear();
958 maybeMailbox.setAddress( addrSpec );
959 result.displayName.clear();
960 result.mailboxList.append( maybeMailbox );
961 return true;
963 scursor = oldscursor;
965 Address maybeAddress;
967 // no, it's not a single mailbox. Try if it's a group:
968 if ( !parseGroup( scursor, send, maybeAddress, isCRLF ) )
970 scursor = oldscursor; // KAlarm: reinstate original scursor on error return
971 return false;
974 result = maybeAddress;
975 return true;
978 } // namespace HeaderParsing
980 // vim: et sw=4: