/* -*- mode: C++; c-file-style: "gnu" -*- Copyright (C) 2009 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net Copyright (c) 2009 Andras Mantia This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kmkernel.h" #include "messagehelper.h" #include "stringutil.h" #include "messageviewer/stringutil.h" #include "version-kmail.h" #include "kmversion.h" #include "templateparser.h" #include "messageinfo.h" #include "mdnadvicedialog.h" #include "mailinglist-magic.h" #include "foldercollection.h" #include #include #include #include #include #include #include #include #include #include #include namespace KMail { namespace MessageHelper { //TODO init them somewhere, now the init code is in KMMsgBase::readConfig() static QStringList sReplySubjPrefixes, sForwardSubjPrefixes; static bool sReplaceSubjPrefix, sReplaceForwSubjPrefix; static const struct { const char * dontAskAgainID; bool canDeny; const char * text; } mdnMessageBoxes[] = { { "mdnNormalAsk", true, I18N_NOOP("This message contains a request to return a notification " "about your reception of the message.\n" "You can either ignore the request or let KMail send a " "\"denied\" or normal response.") }, { "mdnUnknownOption", false, I18N_NOOP("This message contains a request to send a notification " "about your reception of the message.\n" "It contains a processing instruction that is marked as " "\"required\", but which is unknown to KMail.\n" "You can either ignore the request or let KMail send a " "\"failed\" response.") }, { "mdnMultipleAddressesInReceiptTo", true, I18N_NOOP("This message contains a request to send a notification " "about your reception of the message,\n" "but it is requested to send the notification to more " "than one address.\n" "You can either ignore the request or let KMail send a " "\"denied\" or normal response.") }, { "mdnReturnPathEmpty", true, I18N_NOOP("This message contains a request to send a notification " "about your reception of the message,\n" "but there is no return-path set.\n" "You can either ignore the request or let KMail send a " "\"denied\" or normal response.") }, { "mdnReturnPathNotInReceiptTo", true, I18N_NOOP("This message contains a request to send a notification " "about your reception of the message,\n" "but the return-path address differs from the address " "the notification was requested to be sent to.\n" "You can either ignore the request or let KMail send a " "\"denied\" or normal response.") }, }; static const int numMdnMessageBoxes = sizeof mdnMessageBoxes / sizeof *mdnMessageBoxes; static int requestAdviceOnMDN( const char * what ) { for ( int i = 0 ; i < numMdnMessageBoxes ; ++i ) if ( !qstrcmp( what, mdnMessageBoxes[i].dontAskAgainID ) ) { const KCursorSaver saver( Qt::ArrowCursor ); MDNAdviceDialog::MDNAdvice answer; answer = MDNAdviceDialog::questionIgnoreSend( mdnMessageBoxes[i].text, mdnMessageBoxes[i].canDeny ); switch ( answer ) { case MDNAdviceDialog::MDNSend: return 3; case MDNAdviceDialog::MDNSendDenied: return 2; default: case MDNAdviceDialog::MDNIgnore: return 0; } } kWarning() <<"didn't find data for message box \"" << what << "\""; return 0; } QString replaceHeadersInString( KMime::Message *msg, const QString & s ) { QString result = s; QRegExp rx( "\\$\\{([a-z0-9-]+)\\}", Qt::CaseInsensitive ); Q_ASSERT( rx.isValid() ); QRegExp rxDate( "\\$\\{date\\}" ); Q_ASSERT( rxDate.isValid() ); QString sDate = KMime::DateFormatter::formatDate( KMime::DateFormatter::Localized, msg->date()->dateTime().dateTime().toTime_t() ); int idx = 0; if( ( idx = rxDate.indexIn( result, idx ) ) != -1 ) { result.replace( idx, rxDate.matchedLength(), sDate ); } idx = 0; while ( ( idx = rx.indexIn( result, idx ) ) != -1 ) { QString replacement = msg->headerByType( rx.cap(1).toLatin1() ) ? msg->headerByType( rx.cap(1).toLatin1() )->asUnicodeString() : ""; result.replace( idx, rx.matchedLength(), replacement ); idx += replacement.length(); } return result; } void initHeader( KMime::Message* message, uint id ) { applyIdentity( message, id ); message->to()->clear(); message->subject()->clear(); message->date()->setDateTime( KDateTime::currentLocalDateTime() ); // user agent, e.g. KMail/1.9.50 (Windows/5.0; KDE/3.97.1; i686; svn-762186; 2008-01-15) QStringList extraInfo; # if defined KMAIL_SVN_REVISION_STRING && defined KMAIL_SVN_LAST_CHANGE extraInfo << KMAIL_SVN_REVISION_STRING << KMAIL_SVN_LAST_CHANGE; #else #error forgot to include version-kmail.h # endif message->userAgent()->fromUnicodeString( KProtocolManager::userAgentForApplication( "KMail", KMAIL_VERSION, extraInfo ), "utf-8" ); // This will allow to change Content-Type: message->contentType()->setMimeType( "text/plain" ); } void applyIdentity( KMime::Message *message, uint id ) { const KPIMIdentities::Identity & ident = kmkernel->identityManager()->identityForUoidOrDefault( id ); if(ident.fullEmailAddr().isEmpty()) message->from()->clear(); else message->from()->addAddress(ident.fullEmailAddr().toUtf8()); if(ident.replyToAddr().isEmpty()) message->replyTo()->clear(); else message->replyTo()->addAddress(ident.replyToAddr().toUtf8()); if(ident.bcc().isEmpty()) message->bcc()->clear(); else message->bcc()->addAddress(ident.bcc().toUtf8()); if (ident.organization().isEmpty()) message->removeHeader("Organization"); else { KMime::Headers::Generic *header = new KMime::Headers::Generic( "Organization", message, ident.organization(), "utf-8" ); message->setHeader( header ); } if (ident.isDefault()) message->removeHeader("X-KMail-Identity"); else { KMime::Headers::Generic *header = new KMime::Headers::Generic( "X-KMail-Identity", message, QString::number( ident.uoid() ), "utf-8" ); message->setHeader( header ); } if (ident.transport().isEmpty()) message->removeHeader("X-KMail-Transport"); else { KMime::Headers::Generic *header = new KMime::Headers::Generic( "X-KMail-Transport", message, ident.transport(), "utf-8" ); message->setHeader( header ); } if (ident.fcc().isEmpty()) message->removeHeader("X-KMail-Fcc"); else { KMime::Headers::Generic *header = new KMime::Headers::Generic( "X-KMail-Fcc", message, ident.fcc(), "utf-8" ); message->setHeader( header ); } /** TODO Port to KMime if (ident.drafts().isEmpty()) setDrafts( QString() ); else setDrafts( ident.drafts() ); if (ident.templates().isEmpty()) setTemplates( QString() ); else setTemplates( ident.templates() ); */ } KMime::Message* createReply( const Akonadi::Item & item, KMime::Message *origMsg, ReplyStrategy replyStrategy, const QString &selection /*.clear() */, bool noQuote /* = false */, bool allowDecryption /* = true */, bool selectionIsBody /* = false */, const QString &tmpl /* = QString() */ ) { KMime::Message* msg = new KMime::Message; QString str, mailingListStr, replyToStr, toStr; QStringList mailingListAddresses; QByteArray refStr, headerName; bool replyAll = true; initFromMessage( item, msg, origMsg ); MailingList::name(msg, headerName, mailingListStr); replyToStr = origMsg->replyTo()->asUnicodeString(); msg->contentType()->setCharset("utf-8"); Akonadi::Collection parentCollection = item.parentCollection(); FolderCollection *fd = 0; if ( parentCollection.isValid() ) { fd = new FolderCollection( parentCollection, false /*don't save*/ ); if ( fd->isMailingListEnabled() && !fd->mailingListPostAddress().isEmpty() ) { mailingListAddresses << fd->mailingListPostAddress(); } } if ( origMsg->headerByType("List-Post") && origMsg->headerByType("List-Post")->asUnicodeString().contains( "mailto:", Qt::CaseInsensitive ) ) { QString listPost = origMsg->headerByType("List-Post")->asUnicodeString(); QRegExp rx( "]+)@([^>]+)>", Qt::CaseInsensitive ); if ( rx.indexIn( listPost, 0 ) != -1 ) // matched mailingListAddresses << rx.cap(1) + '@' + rx.cap(2); } switch( replyStrategy ) { case ReplySmart : { if ( origMsg->headerByType( "Mail-Followup-To" ) ) { toStr = origMsg->headerByType( "Mail-Followup-To" )->asUnicodeString(); } else if ( !replyToStr.isEmpty() ) { // assume a Reply-To header mangling mailing list toStr = replyToStr; } else if ( !mailingListAddresses.isEmpty() ) { toStr = mailingListAddresses[0]; } else { // doesn't seem to be a mailing list, reply to From: address toStr = origMsg->from()->asUnicodeString(); replyAll = false; } // strip all my addresses from the list of recipients QStringList recipients = KPIMUtils::splitAddressList( toStr ); toStr = StringUtil::stripMyAddressesFromAddressList( recipients ).join(", "); // ... unless the list contains only my addresses (reply to self) if ( toStr.isEmpty() && !recipients.isEmpty() ) toStr = recipients[0]; break; } case ReplyList : { if ( origMsg->headerByType( "Mail-Followup-To" ) ) { toStr = origMsg->headerByType( "Mail-Followup-To" )->asUnicodeString(); } else if ( !mailingListAddresses.isEmpty() ) { toStr = mailingListAddresses[0]; } else if ( !replyToStr.isEmpty() ) { // assume a Reply-To header mangling mailing list toStr = replyToStr; } // strip all my addresses from the list of recipients QStringList recipients = KPIMUtils::splitAddressList( toStr ); toStr = StringUtil::stripMyAddressesFromAddressList( recipients ).join(", "); break; } case ReplyAll : { QStringList recipients; QStringList ccRecipients; // add addresses from the Reply-To header to the list of recipients if( !replyToStr.isEmpty() ) { recipients += KPIMUtils::splitAddressList( replyToStr ); // strip all possible mailing list addresses from the list of Reply-To // addresses for ( QStringList::const_iterator it = mailingListAddresses.constBegin(); it != mailingListAddresses.constEnd(); ++it ) { recipients = MessageViewer::StringUtil::stripAddressFromAddressList( *it, recipients ); } } if ( !mailingListAddresses.isEmpty() ) { // this is a mailing list message if ( recipients.isEmpty() && !origMsg->from()->asUnicodeString().isEmpty() ) { // The sender didn't set a Reply-to address, so we add the From // address to the list of CC recipients. ccRecipients += origMsg->from()->asUnicodeString(); kDebug() << "Added" << origMsg->from()->asUnicodeString() <<"to the list of CC recipients"; } // if it is a mailing list, add the posting address recipients.prepend( mailingListAddresses[0] ); } else { // this is a normal message if ( recipients.isEmpty() && !origMsg->from()->asUnicodeString().isEmpty() ) { // in case of replying to a normal message only then add the From // address to the list of recipients if there was no Reply-to address recipients += origMsg->from()->asUnicodeString(); kDebug() << "Added" << origMsg->from()->asUnicodeString() <<"to the list of recipients"; } } // strip all my addresses from the list of recipients toStr = KMail::StringUtil::stripMyAddressesFromAddressList( recipients ).join(", "); // merge To header and CC header into a list of CC recipients if( !origMsg->cc()->asUnicodeString().isEmpty() || !origMsg->to()->asUnicodeString().isEmpty() ) { QStringList list; if (!origMsg->to()->asUnicodeString().isEmpty()) list += KPIMUtils::splitAddressList(origMsg->to()->asUnicodeString()); if (!origMsg->cc()->asUnicodeString().isEmpty()) list += KPIMUtils::splitAddressList(origMsg->cc()->asUnicodeString()); for( QStringList::ConstIterator it = list.constBegin(); it != list.constEnd(); ++it ) { if( !MessageViewer::StringUtil::addressIsInAddressList( *it, recipients ) && !MessageViewer::StringUtil::addressIsInAddressList( *it, ccRecipients ) ) { ccRecipients += *it; kDebug() << "Added" << *it <<"to the list of CC recipients"; } } } if ( !ccRecipients.isEmpty() ) { // strip all my addresses from the list of CC recipients ccRecipients = StringUtil::stripMyAddressesFromAddressList( ccRecipients ); // in case of a reply to self toStr might be empty. if that's the case // then propagate a cc recipient to To: (if there is any). if ( toStr.isEmpty() && !ccRecipients.isEmpty() ) { toStr = ccRecipients[0]; ccRecipients.pop_front(); } msg->cc()->fromUnicodeString(ccRecipients.join(", "), "utf-8" ); } if ( toStr.isEmpty() && !recipients.isEmpty() ) { // reply to self without other recipients toStr = recipients[0]; } break; } case ReplyAuthor : { if ( !replyToStr.isEmpty() ) { QStringList recipients = KPIMUtils::splitAddressList( replyToStr ); // strip the mailing list post address from the list of Reply-To // addresses since we want to reply in private for ( QStringList::const_iterator it = mailingListAddresses.constBegin(); it != mailingListAddresses.constEnd(); ++it ) { recipients = MessageViewer::StringUtil::stripAddressFromAddressList( *it, recipients ); } if ( !recipients.isEmpty() ) { toStr = recipients.join(", "); } else { // there was only the mailing list post address in the Reply-To header, // so use the From address instead toStr = origMsg->from()->asUnicodeString(); } } else if ( !origMsg->from()->asUnicodeString().isEmpty() ) { toStr = origMsg->from()->asUnicodeString(); } replyAll = false; break; } case ReplyNone : { // the addressees will be set by the caller } } msg->to()->fromUnicodeString( toStr, "utf-8" ); refStr = getRefStr( origMsg ); if (!refStr.isEmpty()) msg->references()->fromUnicodeString( refStr, "utf-8"); //In-Reply-To = original msg-id msg->inReplyTo()->fromUnicodeString( origMsg->messageID()->asUnicodeString(), "utf-8" ); msg->subject()->fromUnicodeString( replySubject(origMsg), "utf-8" ); // If the reply shouldn't be blank, apply the template to the message if ( !noQuote ) { TemplateParser parser( msg, (replyAll ? TemplateParser::ReplyAll : TemplateParser::Reply), selection, /*TODO what the hell is s? s->smartQuote*/ true, allowDecryption, selectionIsBody ); if ( !tmpl.isEmpty() ) parser.process( tmpl, origMsg ); else parser.process( origMsg ); } link( msg, item, KPIM::MessageStatus::statusReplied() ); if ( parentCollection.isValid() && fd->putRepliesInSameFolder() ) { KMime::Headers::Generic *header = new KMime::Headers::Generic( "X-KMail-Fcc", msg, QString::number( parentCollection.id() ), "utf-8" ); msg->setHeader( header ); } delete fd; #if 0 //TODO port to akonadi // replies to an encrypted message should be encrypted as well if ( encryptionState() == KMMsgPartiallyEncrypted || encryptionState() == KMMsgFullyEncrypted ) { msg->setEncryptionState( KMMsgFullyEncrypted ); } #else kDebug() << "AKONADI PORT: Disabled code in " << Q_FUNC_INFO; #endif return msg; } MessageReply createReply2(const Akonadi::Item &item, KMime::Message *origMsg, KMail::ReplyStrategy replyStrategy, const QString &selection /*.clear() */, bool noQuote /* = false */, bool allowDecryption /* = true */, bool selectionIsBody /* = false */, const QString &tmpl /* = QString() */ ) { KMime::Message* msg = new KMime::Message; QString str, mailingListStr, replyToStr, toStr; QStringList mailingListAddresses; QByteArray refStr, headerName; bool replyAll = true; initFromMessage( item, msg, origMsg); MailingList::name(origMsg, headerName, mailingListStr); replyToStr = origMsg->replyTo()->asUnicodeString(); msg->contentType()->setCharset("utf-8"); // determine the mailing list posting address Akonadi::Collection parentCollection = item.parentCollection(); FolderCollection *fd = 0; if ( parentCollection.isValid() ) { fd = new FolderCollection( parentCollection, false /*don't save*/ ); if ( fd->isMailingListEnabled() && !fd->mailingListPostAddress().isEmpty() ) { mailingListAddresses << fd->mailingListPostAddress(); } } if ( origMsg->headerByType("List-Post") && origMsg->headerByType("List-Post")->asUnicodeString().contains( "mailto:", Qt::CaseInsensitive ) ) { QString listPost = origMsg->headerByType("List-Post")->asUnicodeString(); QRegExp rx( "]+)@([^>]+)>", Qt::CaseInsensitive ); if ( rx.indexIn( listPost, 0 ) != -1 ) // matched mailingListAddresses << rx.cap(1) + '@' + rx.cap(2); } switch( replyStrategy ) { case ReplySmart : { if ( origMsg->headerByType( "Mail-Followup-To" ) ) { toStr = origMsg->headerByType( "Mail-Followup-To" )->asUnicodeString(); } else if ( !replyToStr.isEmpty() ) { toStr = replyToStr; // use the ReplyAll template only when it's a reply to a mailing list if ( mailingListAddresses.isEmpty() ) replyAll = false; } else if ( !mailingListAddresses.isEmpty() ) { toStr = mailingListAddresses[0]; } else { // doesn't seem to be a mailing list, reply to From: address toStr = origMsg->from()->asUnicodeString(); replyAll = false; } // strip all my addresses from the list of recipients QStringList recipients = KPIMUtils::splitAddressList( toStr ); toStr = StringUtil::stripMyAddressesFromAddressList( recipients ).join(", "); // ... unless the list contains only my addresses (reply to self) if ( toStr.isEmpty() && !recipients.isEmpty() ) toStr = recipients[0]; break; } case ReplyList : { if ( origMsg->headerByType( "Mail-Followup-To" ) ) { toStr = origMsg->headerByType( "Mail-Followup-To" )->asUnicodeString(); } else if ( !mailingListAddresses.isEmpty() ) { toStr = mailingListAddresses[0]; } else if ( !replyToStr.isEmpty() ) { // assume a Reply-To header mangling mailing list toStr = replyToStr; } // strip all my addresses from the list of recipients QStringList recipients = KPIMUtils::splitAddressList( toStr ); toStr = StringUtil::stripMyAddressesFromAddressList( recipients ).join(", "); break; } case ReplyAll : { QStringList recipients; QStringList ccRecipients; // add addresses from the Reply-To header to the list of recipients if( !replyToStr.isEmpty() ) { recipients += KPIMUtils::splitAddressList( replyToStr ); // strip all possible mailing list addresses from the list of Reply-To // addresses for ( QStringList::const_iterator it = mailingListAddresses.constBegin(); it != mailingListAddresses.constEnd(); ++it ) { recipients = MessageViewer::StringUtil::stripAddressFromAddressList( *it, recipients ); } } if ( !mailingListAddresses.isEmpty() ) { // this is a mailing list message if ( recipients.isEmpty() && !origMsg->from()->asUnicodeString().isEmpty() ) { // The sender didn't set a Reply-to address, so we add the From // address to the list of CC recipients. ccRecipients += origMsg->from()->asUnicodeString(); kDebug() << "Added" << origMsg->from()->asUnicodeString() <<"to the list of CC recipients"; } // if it is a mailing list, add the posting address recipients.prepend( mailingListAddresses[0] ); } else { // this is a normal message if ( recipients.isEmpty() && ! origMsg->from()->asUnicodeString().isEmpty() ) { // in case of replying to a normal message only then add the From // address to the list of recipients if there was no Reply-to address recipients += origMsg->from()->asUnicodeString(); kDebug() << "Added" << origMsg->from()->asUnicodeString() <<"to the list of recipients"; } } // strip all my addresses from the list of recipients toStr = StringUtil::stripMyAddressesFromAddressList( recipients ).join(", "); // merge To header and CC header into a list of CC recipients if( ! origMsg->cc()->asUnicodeString().isEmpty() || ! origMsg->to()->asUnicodeString().isEmpty() ) { QStringList list; if (! origMsg->to()->asUnicodeString().isEmpty()) list += KPIMUtils::splitAddressList(origMsg->to()->asUnicodeString()); if (! origMsg->cc()->asUnicodeString().isEmpty()) list += KPIMUtils::splitAddressList(origMsg->cc()->asUnicodeString()); for( QStringList::ConstIterator it = list.constBegin(); it != list.constEnd(); ++it ) { if( !MessageViewer::StringUtil::addressIsInAddressList( *it, recipients ) && !MessageViewer::StringUtil::addressIsInAddressList( *it, ccRecipients ) ) { ccRecipients += *it; kDebug() << "Added" << *it <<"to the list of CC recipients"; } } } if ( !ccRecipients.isEmpty() ) { // strip all my addresses from the list of CC recipients ccRecipients = StringUtil::stripMyAddressesFromAddressList( ccRecipients ); // in case of a reply to self toStr might be empty. if that's the case // then propagate a cc recipient to To: (if there is any). if ( toStr.isEmpty() && !ccRecipients.isEmpty() ) { toStr = ccRecipients[0]; ccRecipients.pop_front(); } msg->cc()->fromUnicodeString( ccRecipients.join(", "), "utf-8" ); } if ( toStr.isEmpty() && !recipients.isEmpty() ) { // reply to self without other recipients toStr = recipients[0]; } break; } case ReplyAuthor : { if ( !replyToStr.isEmpty() ) { QStringList recipients = KPIMUtils::splitAddressList( replyToStr ); // strip the mailing list post address from the list of Reply-To // addresses since we want to reply in private for ( QStringList::const_iterator it = mailingListAddresses.constBegin(); it != mailingListAddresses.constEnd(); ++it ) { recipients = MessageViewer::StringUtil::stripAddressFromAddressList( *it, recipients ); } if ( !recipients.isEmpty() ) { toStr = recipients.join(", "); } else { // there was only the mailing list post address in the Reply-To header, // so use the From address instead toStr = origMsg->from()->asUnicodeString(); } } else if ( ! origMsg->from()->asUnicodeString().isEmpty() ) { toStr = origMsg->from()->asUnicodeString(); } replyAll = false; break; } case ReplyNone : { // the addressees will be set by the caller } } msg->to()->fromUnicodeString(toStr, "utf-8"); refStr = getRefStr( origMsg ); if (!refStr.isEmpty()) msg->references()->fromUnicodeString (refStr, "utf-8" ); //In-Reply-To = original msg-id msg->inReplyTo()->fromUnicodeString( origMsg->messageID()->asUnicodeString(), "utf-8" ); msg->subject()->fromUnicodeString( replySubject( origMsg ), "utf-8" ); // If the reply shouldn't be blank, apply the template to the message if ( !noQuote ) { TemplateParser parser( msg, (replyAll ? TemplateParser::ReplyAll : TemplateParser::Reply), selection,/*TODO what the hell is s? s->smartQuote*/ true, allowDecryption, selectionIsBody ); if ( !tmpl.isEmpty() ) parser.process( tmpl, origMsg ); else parser.process( origMsg ); } link( msg, item, KPIM::MessageStatus::statusReplied() ); if ( parentCollection.isValid() && fd->putRepliesInSameFolder() ) { KMime::Headers::Generic *header = new KMime::Headers::Generic( "X-KMail-Fcc", msg, QString::number( parentCollection.id() ), "utf-8" ); msg->setHeader( header ); } #if 0 // replies to an encrypted message should be encrypted as well if ( encryptionState() == KMMsgPartiallyEncrypted || encryptionState() == KMMsgFullyEncrypted ) { msg->setEncryptionState( KMMsgFullyEncrypted ); } #else kDebug() << "AKONADI PORT: Disabled code in " << Q_FUNC_INFO; #endif MessageReply reply; reply.msg = msg; reply.replyAll = replyAll; delete fd; return reply; } void link( KMime::Message *msg, const Akonadi::Item & item,const KPIM::MessageStatus& aStatus ) { Q_ASSERT( aStatus.isReplied() || aStatus.isForwarded() || aStatus.isDeleted() ); QString message = msg->headerByType( "X-KMail-Link-Message" ) ? msg->headerByType( "X-KMail-Link-Message" )->asUnicodeString() : QString(); if ( !message.isEmpty() ) message += ','; QString type = msg->headerByType( "X-KMail-Link-Type" ) ? msg->headerByType( "X-KMail-Link-Type" )->asUnicodeString(): QString(); if ( !type.isEmpty() ) type += ','; message += QString::number( item.id() ); if ( aStatus.isReplied() ) type += "reply"; else if ( aStatus.isForwarded() ) type += "forward"; else if ( aStatus.isDeleted() ) type += "deleted"; KMime::Headers::Generic *header = new KMime::Headers::Generic( "X-KMail-Link-Message", msg, message, "utf-8" ); msg->setHeader( header ); header = new KMime::Headers::Generic( "X-KMail-Link-Type", msg, type, "utf-8" ); msg->setHeader( header ); } KMime::Message* createForward( const Akonadi::Item &item, KMime::Message *origMsg, const QString &tmpl /* = QString() */ ) { KMime::Message* msg = new KMime::Message(); // This is a non-multipart, non-text mail (e.g. text/calendar). Construct // a multipart/mixed mail and add the original body as an attachment. if ( !origMsg->contentType()->isMultipart() && ( !origMsg->contentType()->isText() || ( origMsg->contentType()->isText() && origMsg->contentType()->subType() != "html" && origMsg->contentType()->subType() != "plain" ) ) ) { initFromMessage( item, msg, origMsg ); msg->removeHeader("Content-Type"); msg->removeHeader("Content-Transfer-Encoding"); msg->contentType()->setMimeType( "multipart/mixed" ); //TODO: Andras: somebody should check if this is correct. :) // empty text part KMime::Content *msgPart = new KMime::Content; msgPart->contentType()->setMimeType("text/plain"); msg->addContent( msgPart ); // the old contents of the mail KMime::Content *secondPart = new KMime::Content; secondPart->contentType()->setMimeType( origMsg->contentType()->mimeType() ); secondPart->setBody( origMsg->body() ); // use the headers of the original mail secondPart->setHead( origMsg->head() ); msg->addContent( secondPart ); msg->assemble(); //TODO Port it msg->cleanupHeader(); } // Normal message (multipart or text/plain|html) // Just copy the message, the template parser will do the hard work of // replacing the body text in TemplateParser::addProcessedBodyToMessage() else { //TODO Check if this is ok msg->setHead( origMsg->head() ); msg->setBody( origMsg->body() ); QString oldContentType = msg->contentType()->asUnicodeString(); initFromMessage(item, msg, origMsg ); // restore the content type, initFromMessage() sets the contents type to // text/plain, via initHeader(), for unclear reasons msg->contentType()->fromUnicodeString( oldContentType, "utf-8" ); msg->assemble(); } msg->subject()->fromUnicodeString( forwardSubject( origMsg ), "utf-8" ); TemplateParser parser( msg, TemplateParser::Forward, QString(), false, false, false); if ( !tmpl.isEmpty() ) parser.process( tmpl, origMsg ); else parser.process( origMsg ); link( msg, item, KPIM::MessageStatus::statusForwarded() ); return msg; } KMime::Message * createResend( const Akonadi::Item & item, KMime::Message *origMsg ) { KMime::Message *msg = new KMime::Message; initFromMessage( item, msg, origMsg); msg->setContent( origMsg->encodedContent() ); msg->removeHeader( "Message-Id" ); uint originalIdentity = identityUoid( item, origMsg); // Remove all unnecessary headers msg->removeHeader("Bcc"); msg->removeHeader( "Cc" ); msg->removeHeader( "To" ); msg->removeHeader( "Subject" ); // Set the identity from above KMime::Headers::Generic *header = new KMime::Headers::Generic( "X-KMail-Identity", msg, QString::number( originalIdentity ), "utf-8" ); msg->setHeader( header ); // Restore the original bcc field as this is overwritten in applyIdentity msg->bcc( origMsg->bcc() ); return msg; } KMime::Message* createRedirect( const Akonadi::Item & item, KMime::Message *origMsg, const QString &toStr ) { // copy the message 1:1 KMime::Message* msg = new KMime::Message; msg->setContent( origMsg->encodedContent() ); uint id = 0; QString strId = msg->headerByType( "X-KMail-Identity" ) ? msg->headerByType( "X-KMail-Identity" )->asUnicodeString().trimmed() : ""; if ( !strId.isEmpty()) id = strId.toUInt(); const KPIMIdentities::Identity & ident = kmkernel->identityManager()->identityForUoidOrDefault( id ); // X-KMail-Redirect-From: content QString strByWayOf = QString("%1 (by way of %2 <%3>)") .arg( origMsg->from()->asUnicodeString() ) .arg( ident.fullName() ) .arg( ident.emailAddr() ); // Resent-From: content QString strFrom = QString("%1 <%2>") .arg( ident.fullName() ) .arg( ident.emailAddr() ); // format the current date to be used in Resent-Date: QString origDate = msg->date()->asUnicodeString(); msg->date()->setDateTime( KDateTime::currentLocalDateTime() ); QString newDate = msg->date()->asUnicodeString(); // prepend Resent-*: headers (c.f. RFC2822 3.6.6) KMime::Headers::Generic *header = new KMime::Headers::Generic( "Resent-Message-ID", msg, MessageViewer::StringUtil::generateMessageId( msg->sender()->asUnicodeString() ), "utf-8" ); msg->setHeader( header ); header = new KMime::Headers::Generic( "Resent-Date", msg, newDate, "utf-8" ); msg->setHeader( header ); header = new KMime::Headers::Generic( "Resent-To", msg, toStr, "utf-8" ); msg->setHeader( header ); header = new KMime::Headers::Generic( "Resent-To", msg, strFrom, "utf-8" ); msg->setHeader( header ); header = new KMime::Headers::Generic( "X-KMail-Redirect-From", msg, strByWayOf, "utf-8" ); msg->setHeader( header ); header = new KMime::Headers::Generic( "X-KMail-Recipients", msg, toStr, "utf-8" ); msg->setHeader( header ); link( msg, item, KPIM::MessageStatus::statusForwarded() ); return msg; } void initFromMessage(const Akonadi::Item & item, KMime::Message *msg, KMime::Message *origMsg, bool idHeaders) { uint id = identityUoid( item, origMsg ); if ( idHeaders ) initHeader( msg, id ); else { KMime::Headers::Generic *header = new KMime::Headers::Generic( "X-KMail-Identity", msg, QString::number(id), "utf-8" ); msg->setHeader( header ); } if (origMsg->headerByType("X-KMail-Transport")) { KMime::Headers::Generic *header = new KMime::Headers::Generic( "X-KMail-Identity", msg, origMsg->headerByType("X-KMail-Transport")->asUnicodeString(), "utf-8" ); msg->setHeader( header ); } } KMime::Types::AddrSpecList extractAddrSpecs( KMime::Message* msg, const QByteArray & header ) { KMime::Types::AddrSpecList result; if ( !msg->headerByType( header ) ) return result; KMime::Types::AddressList al = MessageViewer::StringUtil::splitAddrField( msg->headerByType( header )->asUnicodeString().toUtf8() ); for ( KMime::Types::AddressList::const_iterator ait = al.constBegin() ; ait != al.constEnd() ; ++ait ) for ( KMime::Types::MailboxList::const_iterator mit = (*ait).mailboxList.constBegin() ; mit != (*ait).mailboxList.constEnd() ; ++mit ) result.push_back( (*mit).addrSpec() ); return result; } QString cleanSubject( KMime::Message* msg ) { return cleanSubject( msg, sReplySubjPrefixes + sForwardSubjPrefixes, true, QString() ).trimmed(); } QString cleanSubject( KMime::Message* msg, const QStringList & prefixRegExps, bool replace, const QString & newPrefix ) { return replacePrefixes( msg->subject()->asUnicodeString(), prefixRegExps, replace, newPrefix ); } QString forwardSubject( KMime::Message* msg ) { return cleanSubject( msg, sForwardSubjPrefixes, sReplaceForwSubjPrefix, "Fwd:" ); } QString replySubject(KMime::Message* msg ) { return cleanSubject( msg, sReplySubjPrefixes, sReplaceSubjPrefix, "Re:" ); } QString replacePrefixes( const QString& str, const QStringList& prefixRegExps, bool replace, const QString& newPrefix ) { bool recognized = false; // construct a big regexp that // 1. is anchored to the beginning of str (sans whitespace) // 2. matches at least one of the part regexps in prefixRegExps QString bigRegExp = QString::fromLatin1("^(?:\\s+|(?:%1))+\\s*") .arg( prefixRegExps.join(")|(?:") ); QRegExp rx( bigRegExp, Qt::CaseInsensitive ); if ( !rx.isValid() ) { kWarning() << "bigRegExp = \"" << bigRegExp << "\"\n" << "prefix regexp is invalid!"; // try good ole Re/Fwd: recognized = str.startsWith( newPrefix ); } else { // valid rx QString tmp = str; if ( rx.indexIn( tmp ) == 0 ) { recognized = true; if ( replace ) return tmp.replace( 0, rx.matchedLength(), newPrefix + ' ' ); } } if ( !recognized ) return newPrefix + ' ' + str; else return str; } uint identityUoid( const Akonadi::Item & item , KMime::Message *msg ) { QString idString; if ( msg->headerByType("X-KMail-Identity") ) idString = msg->headerByType("X-KMail-Identity")->asUnicodeString().trimmed(); bool ok = false; int id = idString.toUInt( &ok ); if ( !ok || id == 0 ) id = kmkernel->identityManager()->identityForAddress( msg->to()->asUnicodeString() + ", " + msg->cc()->asUnicodeString() ).uoid(); if ( id == 0 && item.isValid() ) { if ( item.parentCollection().isValid() ) { FolderCollection fd( item.parentCollection(), false ); id = fd.identity(); } } return id; } KMime::Message* createDeliveryReceipt( const Akonadi::Item & item, KMime::Message *msg ) { QString str, receiptTo; KMime::Message *receipt = 0; receiptTo = msg->headerByType("Disposition-Notification-To") ? msg->headerByType("Disposition-Notification-To")->asUnicodeString() : ""; if ( receiptTo.trimmed().isEmpty() ) return 0; receiptTo.remove( '\n' ); receipt = new KMime::Message; initFromMessage( item, receipt, msg ); receipt->to()->fromUnicodeString( receiptTo, "utf-8" ); receipt->subject()->fromUnicodeString( i18n("Receipt: ") + msg->subject()->asUnicodeString(), "utf-8"); str = "Your message was successfully delivered."; str += "\n\n---------- Message header follows ----------\n"; str += msg->head(); str += "--------------------------------------------\n"; // Conversion to toLatin1 is correct here as Mail headers should contain // ascii only receipt->setBody(str.toLatin1()); setAutomaticFields( receipt ); receipt->assemble(); return receipt; } KMime::Message* createMDN( const Akonadi::Item & item, KMime::Message *msg, KMime::MDN::ActionMode a, KMime::MDN::DispositionType d, bool allowGUI, QList m ) { // RFC 2298: At most one MDN may be issued on behalf of each // particular recipient by their user agent. That is, once an MDN // has been issued on behalf of a recipient, no further MDNs may be // issued on behalf of that recipient, even if another disposition // is performed on the message. //#define MDN_DEBUG 1 #ifndef MDN_DEBUG if ( MessageInfo::instance()->mdnSentState( msg ) != KMMsgMDNStateUnknown && MessageInfo::instance()->mdnSentState( msg ) != KMMsgMDNNone ) return 0; #else char st[2]; st[0] = (char)MessageInfo::instance()->mdnSentState( msg ); st[1] = 0; kDebug() << "mdnSentState() == '" << st <<"'"; #endif // RFC 2298: An MDN MUST NOT be generated in response to an MDN. if ( MessageViewer::ObjectTreeParser::findType( msg, "message", "disposition-notification", true, true ) ) { MessageInfo::instance()->setMDNSentState( msg, KMMsgMDNIgnore ); return 0; } // extract where to send to: QString receiptTo = msg->headerByType("Disposition-Notification-To") ? msg->headerByType("Disposition-Notification-To")->asUnicodeString() : ""; if ( receiptTo.trimmed().isEmpty() ) return 0; receiptTo.remove( '\n' ); KMime::MDN::SendingMode s = KMime::MDN::SentAutomatically; // set to manual if asked user QString special; // fill in case of error, warning or failure KConfigGroup mdnConfig( KMKernel::config(), "MDN" ); // default: int mode = mdnConfig.readEntry( "default-policy", 0 ); if ( !mode || mode < 0 || mode > 3 ) { // early out for ignore: MessageInfo::instance()->setMDNSentState( msg, KMMsgMDNIgnore ); return 0; } if ( mode == 1 /* ask */ && !allowGUI ) return 0; // don't setMDNSentState here! // RFC 2298: An importance of "required" indicates that // interpretation of the parameter is necessary for proper // generation of an MDN in response to this request. If a UA does // not understand the meaning of the parameter, it MUST NOT generate // an MDN with any disposition type other than "failed" in response // to the request. QString notificationOptions = msg->headerByType("Disposition-Notification-Options") ? msg->headerByType("Disposition-Notification-Options")->asUnicodeString() : ""; if ( notificationOptions.contains( "required", Qt::CaseSensitive ) ) { // ### hacky; should parse... // There is a required option that we don't understand. We need to // ask the user what we should do: if ( !allowGUI ) return 0; // don't setMDNSentState here! mode = requestAdviceOnMDN( "mdnUnknownOption" ); s = KMime::MDN::SentManually; special = i18n("Header \"Disposition-Notification-Options\" contained " "required, but unknown parameter"); d = KMime::MDN::Failed; m.clear(); // clear modifiers } // RFC 2298: [ Confirmation from the user SHOULD be obtained (or no // MDN sent) ] if there is more than one distinct address in the // Disposition-Notification-To header. kDebug() << "KPIMUtils::splitAddressList(receiptTo):" // krazy:exclude=kdebug << KPIMUtils::splitAddressList(receiptTo).join("\n"); if ( KPIMUtils::splitAddressList(receiptTo).count() > 1 ) { if ( !allowGUI ) return 0; // don't setMDNSentState here! mode = requestAdviceOnMDN( "mdnMultipleAddressesInReceiptTo" ); s = KMime::MDN::SentManually; } // RFC 2298: MDNs SHOULD NOT be sent automatically if the address in // the Disposition-Notification-To header differs from the address // in the Return-Path header. [...] Confirmation from the user // SHOULD be obtained (or no MDN sent) if there is no Return-Path // header in the message [...] KMime::Types::AddrSpecList returnPathList = extractAddrSpecs( msg, "Return-Path"); QString returnPath = returnPathList.isEmpty() ? QString() : returnPathList.front().localPart + '@' + returnPathList.front().domain ; kDebug() << "clean return path:" << returnPath; if ( returnPath.isEmpty() || !receiptTo.contains( returnPath, Qt::CaseSensitive ) ) { if ( !allowGUI ) return 0; // don't setMDNSentState here! mode = requestAdviceOnMDN( returnPath.isEmpty() ? "mdnReturnPathEmpty" : "mdnReturnPathNotInReceiptTo" ); s = KMime::MDN::SentManually; } if ( true ) { if ( mode == 1 ) { // ask assert( allowGUI ); mode = requestAdviceOnMDN( "mdnNormalAsk" ); s = KMime::MDN::SentManually; // asked user } switch ( mode ) { case 0: // ignore: MessageInfo::instance()->setMDNSentState( msg, KMMsgMDNIgnore ); return 0; default: case 1: kFatal() << "The \"ask\" mode should never appear here!"; break; case 2: // deny d = KMime::MDN::Denied; m.clear(); break; case 3: break; } } // extract where to send from: QString finalRecipient = kmkernel->identityManager() ->identityForUoidOrDefault( identityUoid( item, msg ) ).fullEmailAddr(); // // Generate message: // KMime::Message * receipt = new KMime::Message(); initFromMessage( item, receipt, msg); receipt->contentType()->from7BitString( "multipart/report" ); receipt->removeHeader("Content-Transfer-Encoding"); // Modify the ContentType directly (replaces setAutomaticFields(true)) receipt->contentType()->setParameter( "report-type", "disposition-notification" ); QString description = replaceHeadersInString( msg, KMime::MDN::descriptionFor( d, m ) ); // text/plain part: KMime::Content* firstMsgPart = new KMime::Content( msg ); firstMsgPart->contentType()->setMimeType( "text/plain" ); firstMsgPart->setBody( description.toUtf8() ); receipt->addContent( firstMsgPart ); // message/disposition-notification part: KMime::Content* secondMsgPart = new KMime::Content( msg ); secondMsgPart->contentType()->setMimeType( "message/disposition-notification" ); //secondMsgPart.setCharset( "us-ascii" ); //secondMsgPart.setCteStr( "7bit" ); secondMsgPart->setBody( KMime::MDN::dispositionNotificationBodyContent( finalRecipient, msg->headerByType("Original-Recipient") ? msg->headerByType("Original-Recipient")->as7BitString() : "", msg->messageID()->as7BitString(), /* Message-ID */ d, a, s, m, special ) ); receipt->addContent( secondMsgPart ); // message/rfc822 or text/rfc822-headers body part: int num = mdnConfig.readEntry( "quote-message", 0 ); if ( num < 0 || num > 2 ) num = 0; /* 0=> Nothing, 1=>Full Message, 2=>HeadersOnly*/ KMime::Content* thirdMsgPart = new KMime::Content( msg ); switch ( num ) { case 1: thirdMsgPart->contentType()->setMimeType( "message/rfc822" ); thirdMsgPart->setBody( asSendableString( msg ) ); receipt->addContent( thirdMsgPart ); break; case 2: thirdMsgPart->contentType()->setMimeType( "text/rfc822-headers" ); thirdMsgPart->setBody( headerAsSendableString( msg ) ); receipt->addContent( thirdMsgPart ); break; case 0: default: break; }; receipt->to()->fromUnicodeString( receiptTo, "utf-8" ); receipt->subject()->from7BitString( "Message Disposition Notification" ); KMime::Headers::Generic *header = new KMime::Headers::Generic( "In-Reply-To", receipt, msg->messageID()->as7BitString() ); receipt->setHeader( header ); receipt->references()->from7BitString( getRefStr( msg ) ); receipt->assemble(); kDebug() << "final message:" + receipt->body();//TODO port asString(); // // Set "MDN sent" status: // KMMsgMDNSentState state = KMMsgMDNStateUnknown; switch ( d ) { case KMime::MDN::Displayed: state = KMMsgMDNDisplayed; break; case KMime::MDN::Deleted: state = KMMsgMDNDeleted; break; case KMime::MDN::Dispatched: state = KMMsgMDNDispatched; break; case KMime::MDN::Processed: state = KMMsgMDNProcessed; break; case KMime::MDN::Denied: state = KMMsgMDNDenied; break; case KMime::MDN::Failed: state = KMMsgMDNFailed; break; }; MessageInfo::instance()->setMDNSentState( msg, state ); receipt->assemble(); return receipt; } void setAutomaticFields(KMime::Message *msg, bool aIsMulti) { //TODO review and port header.MimeVersion().FromString("1.0"); if (aIsMulti || msg->contents().size() > 1) { // Set the type to 'Multipart' and the subtype to 'Mixed' msg->contentType()->setMimeType( "multipart/mixed" ); // Create a random printable string and set it as the boundary parameter //TODO review and port contentType.CreateBoundary(0); } } QByteArray asSendableString( KMime::Message *msg ) { KMime::Message message; message.setContent( msg->encodedContent() ); removePrivateHeaderFields( &message ); message.removeHeader("Bcc"); return message.encodedContent(); } QByteArray headerAsSendableString( KMime::Message *msg ) { KMime::Message message; message.setContent( msg->encodedContent() ); removePrivateHeaderFields( &message ); message.removeHeader("Bcc"); return message.head(); } void removePrivateHeaderFields( KMime::Message *msg ) { msg->removeHeader("Status"); msg->removeHeader("X-Status"); msg->removeHeader("X-KMail-EncryptionState"); msg->removeHeader("X-KMail-SignatureState"); msg->removeHeader("X-KMail-MDN-Sent"); msg->removeHeader("X-KMail-Transport"); msg->removeHeader("X-KMail-Identity"); msg->removeHeader("X-KMail-Fcc"); msg->removeHeader("X-KMail-Redirect-From"); msg->removeHeader("X-KMail-Link-Message"); msg->removeHeader("X-KMail-Link-Type"); msg->removeHeader( "X-KMail-QuotePrefix" ); } QByteArray getRefStr( KMime::Message *msg ) { QByteArray firstRef, lastRef, refStr, retRefStr; int i, j; refStr = msg->headerByType("References") ? msg->headerByType("References")->as7BitString().trimmed() : ""; if (refStr.isEmpty()) return msg->messageID()->as7BitString(); i = refStr.indexOf('<'); j = refStr.indexOf('>'); firstRef = refStr.mid(i, j-i+1); if (!firstRef.isEmpty()) retRefStr = firstRef + ' '; i = refStr.lastIndexOf('<'); j = refStr.lastIndexOf('>'); lastRef = refStr.mid(i, j-i+1); if (!lastRef.isEmpty() && lastRef != firstRef) retRefStr += lastRef + ' '; retRefStr += msg->messageID()->as7BitString(); return retRefStr; } QString msgId(KMime::Message *msg) { if ( !msg->headerByType("Message-Id") ) return QString(); QString msgId = msg->headerByType("Message-Id")->asUnicodeString(); // search the end of the message id const int rightAngle = msgId.indexOf( '>' ); if (rightAngle != -1) msgId.truncate( rightAngle + 1 ); // now search the start of the message id const int leftAngle = msgId.lastIndexOf( '<' ); if (leftAngle != -1) msgId = msgId.mid( leftAngle ); return msgId; } QString ccStrip( KMime::Message* msg ) { return MessageViewer::StringUtil::stripEmailAddr( msg->cc()->asUnicodeString() ); } QString toStrip( KMime::Message* msg ) { return MessageViewer::StringUtil::stripEmailAddr( msg->to()->asUnicodeString() ); } QString fromStrip( KMime::Message* msg ) { return MessageViewer::StringUtil::stripEmailAddr( msg->from()->asUnicodeString() ); } QString stripOffPrefixes( const QString& str ) { return replacePrefixes( str, sReplySubjPrefixes + sForwardSubjPrefixes, true, QString() ).trimmed(); } QString skipKeyword( const QString& aStr, QChar sepChar, bool* hasKeyword) { QString str = aStr; while (str[0] == ' ') str.remove(0,1); if (hasKeyword) *hasKeyword=false; unsigned int i = 0, maxChars = 3; unsigned int strLength(str.length()); for (i=0; i < strLength && i < maxChars; i++) { if (str[i] < 'A' || str[i] == sepChar) break; } if (str[i] == sepChar) // skip following spaces too { do { i++; } while (str[i] == ' '); if (hasKeyword) *hasKeyword=true; return str.mid(i); } return str; } } }