1 /* Copyright 2009 Thomas McGuire <mcguire@kde.org>
3 This program is free software; you can redistribute it and/or
4 modify it under the terms of the GNU General Public License as
5 published by the Free Software Foundation; either version 2 of
6 the License or (at your option) version 3 or any later version
7 accepted by the membership of KDE e.V. (or its successor approved
8 by the membership of KDE e.V.), which shall act as a proxy
9 defined in Section 14 of version 3 of the license.
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
17 along with this program. If not, see <http://www.gnu.org/licenses/>.
19 #include "stringutil.h"
21 #include <kmime/kmime_charfreq.h>
22 #include <kmime/kmime_header_parsing.h>
23 #include <kmime/kmime_util.h>
24 #include <kmime/kmime_headers.h>
25 #include <kmime/kmime_message.h>
26 #include <KPIMUtils/Email>
27 #include <KPIMIdentities/IdentityManager>
30 #include <KConfigGroup>
38 #include <QStringList>
40 using namespace KMime
;
41 using namespace KMime::Types
;
42 using namespace KMime::HeaderParsing
;
49 // Removes trailing spaces and tabs at the end of the line
50 static void removeTrailingSpace( QString
&line
)
52 int i
= line
.length() - 1 ;
53 while( ( i
>= 0 ) && ( ( line
[i
] == ' ' ) || ( line
[i
] == '\t' ) ) )
55 line
.truncate( i
+ 1 );
58 // Spilts the line off in two parts: The quote prefixes and the actual text of the line.
59 // For example, for the string "> > > Hello", it would be split up in "> > > " as the quote
60 // prefix, and "Hello" as the actual text.
61 // The actual text is written back to the "line" parameter, and the quote prefix is returned.
62 static QString
splitLine( QString
&line
)
64 removeTrailingSpace( line
);
66 int startOfActualText
= -1;
68 // TODO: Replace tabs with spaces first.
70 // Loop through the chars in the line to find the place where the quote prefix stops
71 while( i
< line
.length() )
73 const QChar c
= line
[i
];
74 const bool isAllowedQuoteChar
= (c
== '>') || (c
== ':') || (c
== '|') ||
75 (c
== ' ') || (c
== '\t');
76 if ( isAllowedQuoteChar
)
77 startOfActualText
= i
+ 1;
83 // If the quote prefix only consists of whitespace, don't consider it as a quote prefix at all
84 if ( line
.left( startOfActualText
).trimmed().isEmpty() )
85 startOfActualText
= 0;
87 // No quote prefix there -> nothing to do
88 if ( startOfActualText
<= 0 )
93 // Entire line consists of only the quote prefix
94 if ( i
== line
.length() )
96 const QString quotePrefix
= line
.left( startOfActualText
);
101 // Line contains both the quote prefix and the actual text, really split it up now
102 const QString quotePrefix
= line
.left( startOfActualText
);
103 line
= line
.mid( startOfActualText
);
107 // Changes the given text so that each line of it fits into the given maximal length.
108 // At each line, the "indent" string is prepended, which is usually the quote prefix.
109 // The text parameter will be empty afterwards.
111 // text = "Hello World, this is a test."
114 // Result: "> Hello World,\n"
115 // "> this is a test."
116 static QString
flowText( QString
&text
, const QString
& indent
, int maxLength
)
119 if ( text
.isEmpty() ) {
120 return indent
+ "\n";
124 while ( !text
.isEmpty() )
126 // Find the next point in the text where we have to do a line break. Start searching
127 // at maxLength position and then walk backwards looking for a space
129 if ( text
.length() > maxLength
)
131 breakPosition
= maxLength
;
132 while( ( breakPosition
>= 0 ) && ( text
[breakPosition
] != ' ' ) )
134 if ( breakPosition
<= 0 ) {
135 // Couldn't break before maxLength.
136 breakPosition
= maxLength
;
140 breakPosition
= text
.length();
143 QString line
= text
.left( breakPosition
);
144 if ( breakPosition
< text
.length() )
145 text
= text
.mid( breakPosition
);
149 // Strip leading whitespace of new lines, since that looks strange
150 if ( !result
.isEmpty() && line
.startsWith( ' ' ) )
151 line
= line
.mid( 1 );
153 result
+= indent
+ line
+ '\n';
159 // Writes all lines/text parts contained in the "textParts" list to the output text, "msg".
160 // Quote characters are added in front of each line, and no line is longer than
163 // Although the lines in textParts are considered separate lines, they can actually be run
164 // together into a single line in some cases. This is basically the main difference to flowText().
167 // textParts = "Hello World, this is a test.", "Really"
170 // Result: "> Hello World, this\n
171 // > is a test. Really"
172 // Notice how in this example, the text line "Really" is no longer a separate line, it was run
173 // together with a previously broken line.
175 // "textParts" is cleared upon return.
176 static bool flushPart( QString
&msg
, QStringList
&textParts
,
177 const QString
&indent
, int maxLength
)
179 maxLength
-= indent
.length();
180 if ( maxLength
< 20 )
183 // Remove empty lines at end of quote
184 while ( !textParts
.isEmpty() && textParts
.last().isEmpty() ) {
185 textParts
.removeLast();
189 foreach( const QString line
, textParts
) {
191 // An empty line in the input means that an empty line should be in the output as well.
192 // Therefore, we write all of our text so far to the msg.
193 if ( line
.isEmpty() ) {
194 if ( !text
.isEmpty() )
195 msg
+= flowText( text
, indent
, maxLength
);
196 msg
+= indent
+ '\n';
200 if ( text
.isEmpty() )
203 text
+= ' ' + line
.trimmed();
205 // If the line doesn't need to be wrapped at all, just write it out as-is.
206 // When a line exceeds the maximum length and therefore needs to be broken, this statement
207 // if false, and therefore we keep adding lines to our text, so they get ran together in the
208 // next flowText call, as "text" contains several text parts/lines then.
209 if ( ( text
.length() < maxLength
) || ( line
.length() < ( maxLength
- 10 ) ) )
210 msg
+= flowText( text
, indent
, maxLength
);
214 // Write out pending text to the msg
215 if ( !text
.isEmpty() )
216 msg
+= flowText( text
, indent
, maxLength
);
218 const bool appendEmptyLine
= !textParts
.isEmpty();
220 return appendEmptyLine
;
223 QStringList
stripMyAddressesFromAddressList( const QStringList
& list
, const KPIMIdentities::IdentityManager
* identMan
)
225 QStringList addresses
= list
;
226 for( QStringList::Iterator it
= addresses
.begin();
227 it
!= addresses
.end(); ) {
228 kDebug() << "Check whether" << *it
<<"is one of my addresses";
229 if( identMan
->thatIsMe( KPIMUtils::extractEmailAddress( *it
) ) ) {
230 kDebug() << "Removing" << *it
<<"from the address list";
231 it
= addresses
.erase( it
);
239 QMap
<QString
, QString
> parseMailtoUrl ( const KUrl
& url
)
241 kDebug() << url
.pathOrUrl();
242 QMap
<QString
, QString
> values
= url
.queryItems( KUrl::CaseInsensitiveKeys
);
243 QString to
= KPIMUtils::decodeMailtoUrl( url
);
244 to
= to
.isEmpty() ? values
.value( "to" ) : to
+ QString( ", " ) + values
.value( "to" );
245 values
.insert( "to", to
);
249 QString
stripSignature ( const QString
& msg
, bool clearSigned
)
251 // Following RFC 3676, only > before --
252 // I prefer to not delete a SB instead of delete good mail content.
253 const QRegExp sbDelimiterSearch
= clearSigned
?
254 QRegExp( "(^|\n)[> ]*--\\s?\n" ) : QRegExp( "(^|\n)[> ]*-- \n" );
255 // The regular expression to look for prefix change
256 const QRegExp commonReplySearch
= QRegExp( "^[ ]*>" );
259 int posDeletingStart
= 1; // to start looking at 0
261 // While there are SB delimiters (start looking just before the deleted SB)
262 while ( ( posDeletingStart
= res
.indexOf( sbDelimiterSearch
, posDeletingStart
-1 ) ) >= 0 )
264 QString prefix
; // the current prefix
265 QString line
; // the line to check if is part of the SB
267 int posSignatureBlock
= -1;
268 // Look for the SB beginning
269 posSignatureBlock
= res
.indexOf( '-', posDeletingStart
);
270 // The prefix before "-- "$
271 if ( res
[posDeletingStart
] == '\n' ) ++posDeletingStart
;
272 prefix
= res
.mid( posDeletingStart
, posSignatureBlock
- posDeletingStart
);
273 posNewLine
= res
.indexOf( '\n', posSignatureBlock
) + 1;
275 // now go to the end of the SB
276 while ( posNewLine
< res
.size() && posNewLine
> 0 )
278 // handle the undefined case for mid ( x , -n ) where n>1
279 int nextPosNewLine
= res
.indexOf( '\n', posNewLine
);
280 if ( nextPosNewLine
< 0 ) nextPosNewLine
= posNewLine
- 1;
281 line
= res
.mid( posNewLine
, nextPosNewLine
- posNewLine
);
283 // check when the SB ends:
284 // * does not starts with prefix or
285 // * starts with prefix+(any substring of prefix)
286 if ( ( prefix
.isEmpty() && line
.indexOf( commonReplySearch
) < 0 ) ||
287 ( !prefix
.isEmpty() && line
.startsWith( prefix
) &&
288 line
.mid( prefix
.size() ).indexOf( commonReplySearch
) < 0 ) )
290 posNewLine
= res
.indexOf( '\n', posNewLine
) + 1;
293 break; // end of the SB
295 // remove the SB or truncate when is the last SB
296 if ( posNewLine
> 0 )
297 res
.remove( posDeletingStart
, posNewLine
- posDeletingStart
);
299 res
.truncate( posDeletingStart
);
304 AddressList
splitAddrField( const QByteArray
& str
)
307 const char * scursor
= str
.begin();
309 return AddressList();
310 const char * const send
= str
.begin() + str
.length();
311 if ( !parseAddressList( scursor
, send
, result
) )
312 kDebug() << "Error in address splitting: parseAddressList returned false!";
316 QString
generateMessageId( const QString
& addr
, const QString
&msgIdSuffix
)
318 const QDateTime datetime
= QDateTime::currentDateTime();
320 QString msgIdStr
= '<' + datetime
.toString( "yyyyMMddhhmm.sszzz" );
322 if( !msgIdSuffix
.isEmpty() )
323 msgIdStr
+= '@' + msgIdSuffix
;
325 msgIdStr
+= '.' + KPIMUtils::toIdn( addr
);
332 QByteArray
html2source( const QByteArray
& src
)
334 QByteArray
result( 1 + 6*src
.length(), '\0' ); // maximal possible length
336 QByteArray::ConstIterator s
= src
.begin();
337 QByteArray::Iterator d
= result
.begin();
400 result
.truncate( d
- result
.begin() );
404 QByteArray
stripEmailAddr( const QByteArray
& aStr
)
406 if ( aStr
.isEmpty() )
411 // The following is a primitive parser for a mailbox-list (cf. RFC 2822).
412 // The purpose is to extract a displayable string from the mailboxes.
413 // Comments in the addr-spec are not handled. No error checking is done.
417 QByteArray angleAddress
;
418 enum { TopLevel
, InComment
, InAngleAddress
} context
= TopLevel
;
419 bool inQuotedString
= false;
420 int commentLevel
= 0;
422 for ( const char* p
= aStr
.data(); *p
; ++p
) {
426 case '"' : inQuotedString
= !inQuotedString
;
428 case '(' : if ( !inQuotedString
) {
435 case '<' : if ( !inQuotedString
) {
436 context
= InAngleAddress
;
441 case '\\' : // quoted character
446 case ',' : if ( !inQuotedString
) {
447 // next email address
448 if ( !result
.isEmpty() )
450 name
= name
.trimmed();
451 comment
= comment
.trimmed();
452 angleAddress
= angleAddress
.trimmed();
453 if ( angleAddress
.isEmpty() && !comment
.isEmpty() ) {
454 // handle Outlook-style addresses like
455 // john.doe@invalid (John Doe)
458 else if ( !name
.isEmpty() ) {
461 else if ( !comment
.isEmpty() ) {
464 else if ( !angleAddress
.isEmpty() ) {
465 result
+= angleAddress
;
468 comment
= QByteArray();
469 angleAddress
= QByteArray();
474 default : name
+= *p
;
480 case '(' : ++commentLevel
;
483 case ')' : --commentLevel
;
484 if ( commentLevel
== 0 ) {
486 comment
+= ' '; // separate the text of several comments
491 case '\\' : // quoted character
496 default : comment
+= *p
;
500 case InAngleAddress
: {
502 case '"' : inQuotedString
= !inQuotedString
;
505 case '>' : if ( !inQuotedString
) {
511 case '\\' : // quoted character
516 default : angleAddress
+= *p
;
520 } // switch ( context )
522 if ( !result
.isEmpty() )
524 name
= name
.trimmed();
525 comment
= comment
.trimmed();
526 angleAddress
= angleAddress
.trimmed();
527 if ( angleAddress
.isEmpty() && !comment
.isEmpty() ) {
528 // handle Outlook-style addresses like
529 // john.doe@invalid (John Doe)
532 else if ( !name
.isEmpty() ) {
535 else if ( !comment
.isEmpty() ) {
538 else if ( !angleAddress
.isEmpty() ) {
539 result
+= angleAddress
;
542 //kDebug() << "Returns \"" << result << "\"";
546 QString
stripEmailAddr( const QString
& aStr
)
548 //kDebug() << "(" << aStr << ")";
550 if ( aStr
.isEmpty() )
555 // The following is a primitive parser for a mailbox-list (cf. RFC 2822).
556 // The purpose is to extract a displayable string from the mailboxes.
557 // Comments in the addr-spec are not handled. No error checking is done.
561 QString angleAddress
;
562 enum { TopLevel
, InComment
, InAngleAddress
} context
= TopLevel
;
563 bool inQuotedString
= false;
564 int commentLevel
= 0;
567 int strLength(aStr
.length());
568 for ( int index
= 0; index
< strLength
; ++index
) {
572 switch ( ch
.toLatin1() ) {
573 case '"' : inQuotedString
= !inQuotedString
;
575 case '(' : if ( !inQuotedString
) {
582 case '<' : if ( !inQuotedString
) {
583 context
= InAngleAddress
;
588 case '\\' : // quoted character
589 ++index
; // skip the '\'
590 if ( index
< aStr
.length() )
593 case ',' : if ( !inQuotedString
) {
594 // next email address
595 if ( !result
.isEmpty() )
597 name
= name
.trimmed();
598 comment
= comment
.trimmed();
599 angleAddress
= angleAddress
.trimmed();
600 if ( angleAddress
.isEmpty() && !comment
.isEmpty() ) {
601 // handle Outlook-style addresses like
602 // john.doe@invalid (John Doe)
605 else if ( !name
.isEmpty() ) {
608 else if ( !comment
.isEmpty() ) {
611 else if ( !angleAddress
.isEmpty() ) {
612 result
+= angleAddress
;
616 angleAddress
.clear();
621 default : name
+= ch
;
626 switch ( ch
.toLatin1() ) {
627 case '(' : ++commentLevel
;
630 case ')' : --commentLevel
;
631 if ( commentLevel
== 0 ) {
633 comment
+= ' '; // separate the text of several comments
638 case '\\' : // quoted character
639 ++index
; // skip the '\'
640 if ( index
< aStr
.length() )
641 comment
+= aStr
[index
];
643 default : comment
+= ch
;
647 case InAngleAddress
: {
648 switch ( ch
.toLatin1() ) {
649 case '"' : inQuotedString
= !inQuotedString
;
652 case '>' : if ( !inQuotedString
) {
658 case '\\' : // quoted character
659 ++index
; // skip the '\'
660 if ( index
< aStr
.length() )
661 angleAddress
+= aStr
[index
];
663 default : angleAddress
+= ch
;
667 } // switch ( context )
669 if ( !result
.isEmpty() )
671 name
= name
.trimmed();
672 comment
= comment
.trimmed();
673 angleAddress
= angleAddress
.trimmed();
674 if ( angleAddress
.isEmpty() && !comment
.isEmpty() ) {
675 // handle Outlook-style addresses like
676 // john.doe@invalid (John Doe)
679 else if ( !name
.isEmpty() ) {
682 else if ( !comment
.isEmpty() ) {
685 else if ( !angleAddress
.isEmpty() ) {
686 result
+= angleAddress
;
689 //kDebug() << "Returns \"" << result << "\"";
693 QString
quoteHtmlChars( const QString
& str
, bool removeLineBreaks
)
697 unsigned int strLength(str
.length());
698 result
.reserve( 6*strLength
); // maximal possible length
699 for( unsigned int i
= 0; i
< strLength
; ++i
) {
700 switch ( str
[i
].toLatin1() ) {
714 if ( !removeLineBreaks
)
729 void removePrivateHeaderFields( const KMime::Message::Ptr
&msg
) {
730 msg
->removeHeader("Status");
731 msg
->removeHeader("X-Status");
732 msg
->removeHeader("X-KMail-EncryptionState");
733 msg
->removeHeader("X-KMail-SignatureState");
734 msg
->removeHeader("X-KMail-MDN-Sent");
735 msg
->removeHeader("X-KMail-Transport");
736 msg
->removeHeader("X-KMail-Identity");
737 msg
->removeHeader("X-KMail-Fcc");
738 msg
->removeHeader("X-KMail-Redirect-From");
739 msg
->removeHeader("X-KMail-Link-Message");
740 msg
->removeHeader("X-KMail-Link-Type");
741 msg
->removeHeader("X-KMail-QuotePrefix");
742 msg
->removeHeader("X-KMail-CursorPos");
743 msg
->removeHeader( "X-KMail-Templates" );
744 msg
->removeHeader( "X-KMail-Drafts" );
745 msg
->removeHeader( "X-KMail-Tag" );
748 QByteArray
asSendableString( const KMime::Message::Ptr
&msg
)
750 KMime::Message message
;
751 message
.setContent( msg
->encodedContent() );
752 removePrivateHeaderFields( KMime::Message::Ptr( &message
) );
753 message
.removeHeader("Bcc");
754 return message
.encodedContent();
757 QByteArray
headerAsSendableString( const KMime::Message::Ptr
&msg
)
759 KMime::Message message
;
760 message
.setContent( msg
->encodedContent() );
761 removePrivateHeaderFields( KMime::Message::Ptr( &message
) );
762 message
.removeHeader("Bcc");
763 return message
.head();
767 QString
emailAddrAsAnchor( const KMime::Types::Mailbox::List
&mailboxList
,
768 Display display
, const QString
& cssStyle
,
769 Link link
, AddressMode expandable
, const QString
& fieldName
,
773 int numberAddresses
= 0;
774 bool expandableInserted
= false;
777 foreach( KMime::Types::Mailbox mailbox
, mailboxList
) {
778 if( !mailbox
.prettyAddress().isEmpty() ) {
780 if( expandable
== ExpandableAddresses
&& !expandableInserted
&& numberAddresses
> collapseNumber
) {
781 result
= "<span id=\"icon" + fieldName
+ "\"></span>" + result
;
782 result
+= "<span id=\"dots" + fieldName
+ "\">...</span><span id=\"hidden" + fieldName
+"\">";
783 expandableInserted
= true;
786 if( link
== ShowLink
) {
787 result
+= "<a href=\"mailto:"
788 + KUrl::toPercentEncoding( KPIMUtils::encodeMailtoUrl( mailbox
.prettyAddress( KMime::Types::Mailbox::QuoteWhenNecessary
) ).path() )
789 + "\" "+cssStyle
+">";
791 if ( display
== DisplayNameOnly
) {
792 if ( !mailbox
.name().isEmpty() ) // Fallback to the email address when the name is not set.
793 result
+= quoteHtmlChars( mailbox
.name(), true );
795 result
+= quoteHtmlChars( mailbox
.prettyAddress(), true );
797 result
+= quoteHtmlChars( mailbox
.prettyAddress( KMime::Types::Mailbox::QuoteWhenNecessary
), true );
799 if( link
== ShowLink
) {
805 // cut of the trailing ", "
806 if( link
== ShowLink
) {
807 result
.truncate( result
.length() - 2 );
810 if( expandableInserted
) {
816 QString
emailAddrAsAnchor( KMime::Headers::Generics::MailboxList
*mailboxList
,
817 Display display
, const QString
& cssStyle
,
818 Link link
, AddressMode expandable
, const QString
& fieldName
,
821 Q_ASSERT( mailboxList
);
822 return emailAddrAsAnchor( mailboxList
->mailboxes(), display
, cssStyle
, link
, expandable
, fieldName
, collapseNumber
);
825 QString
emailAddrAsAnchor( KMime::Headers::Generics::AddressList
*addressList
,
826 Display display
, const QString
& cssStyle
,
827 Link link
, AddressMode expandable
, const QString
& fieldName
,
830 Q_ASSERT( addressList
);
831 return emailAddrAsAnchor( addressList
->mailboxes(), display
, cssStyle
, link
, expandable
, fieldName
, collapseNumber
);
834 QStringList
stripAddressFromAddressList( const QString
& address
,
835 const QStringList
& list
)
837 QStringList
addresses( list
);
838 QString
addrSpec( KPIMUtils::extractEmailAddress( address
) );
839 for ( QStringList::Iterator it
= addresses
.begin();
840 it
!= addresses
.end(); ) {
841 if ( kasciistricmp( addrSpec
.toUtf8().data(),
842 KPIMUtils::extractEmailAddress( *it
).toUtf8().data() ) == 0 ) {
843 kDebug() << "Removing" << *it
<< "from the address list";
844 it
= addresses
.erase( it
);
852 bool addressIsInAddressList( const QString
& address
,
853 const QStringList
& addresses
)
855 QString addrSpec
= KPIMUtils::extractEmailAddress( address
);
856 for( QStringList::ConstIterator it
= addresses
.begin();
857 it
!= addresses
.end(); ++it
) {
858 if ( kasciistricmp( addrSpec
.toUtf8().data(),
859 KPIMUtils::extractEmailAddress( *it
).toUtf8().data() ) == 0 )
865 QString
guessEmailAddressFromLoginName( const QString
& loginName
)
867 if ( loginName
.isEmpty() )
870 QString address
= loginName
;
872 address
+= QHostInfo::localHostName();
874 // try to determine the real name
875 const KUser
user( loginName
);
876 if ( user
.isValid() ) {
877 QString fullName
= user
.property( KUser::FullName
).toString();
878 if ( fullName
.contains( QRegExp( "[^ 0-9A-Za-z\\x0080-\\xFFFF]" ) ) )
879 address
= '"' + fullName
.replace( '\\', "\\" ).replace( '"', "\\" )
880 + "\" <" + address
+ '>';
882 address
= fullName
+ " <" + address
+ '>';
888 QString
smartQuote( const QString
&msg
, int maxLineLength
)
890 // The algorithm here is as follows:
891 // We split up the incoming msg into lines, and then iterate over each line.
892 // We keep adding lines with the same indent ( = quote prefix, e.g. "> " ) to a
893 // "textParts" list. So the textParts list contains only lines with the same quote
896 // When all lines with the same indent are collected in "textParts", we write those out
897 // to the result by calling flushPart(), which does all the nice formatting for us.
899 QStringList textParts
;
901 bool firstPart
= true;
903 foreach ( QString line
, msg
.split( '\n' ) ) {
905 // Split off the indent from the line
906 const QString indent
= splitLine( line
);
908 if ( line
.isEmpty() ) {
910 textParts
.append( QString() );
919 // The indent changed, that means we have to write everything contained in textParts to the
920 // result, which we do by calling flushPart().
921 if ( oldIndent
!= indent
) {
923 // Check if the last non-blank line is a "From" line. A from line is the line containing the
924 // attribution to a quote, e.g. "Yesterday, you wrote:". We'll just check for the last colon
925 // here, to simply things.
926 // If there is a From line, remove it from the textParts to that flushPart won't break it.
927 // We'll manually add it to the result afterwards.
929 if ( !textParts
.isEmpty() ) {
930 for ( int i
= textParts
.count() - 1; i
>= 0; i
-- ) {
932 // Check if we have found the From line
933 if ( textParts
[i
].endsWith( ':' ) ) {
934 fromLine
= oldIndent
+ textParts
[i
] + '\n';
935 textParts
.removeAt( i
);
939 // Abort on first non-empty line
940 if ( !textParts
[i
].trimmed().isEmpty() )
945 // Write out all lines with the same indent using flushPart(). The textParts list
946 // is cleared for us.
947 if ( flushPart( result
, textParts
, oldIndent
, maxLineLength
) ) {
948 if ( oldIndent
.length() > indent
.length() )
949 result
+= indent
+ '\n';
951 result
+= oldIndent
+ '\n';
954 if ( !fromLine
.isEmpty() ) {
961 textParts
.append( line
);
964 // Write out anything still pending
965 flushPart( result
, textParts
, oldIndent
, maxLineLength
);
967 // Remove superfluous newline which was appended in flowText
968 if ( !result
.isEmpty() && result
.endsWith( '\n' ) )
974 bool isCryptoPart( const QString
&type
, const QString
&subType
, const QString
&fileName
)
976 return ( type
.toLower() == "application" &&
977 ( subType
.toLower() == "pgp-encrypted" ||
978 subType
.toLower() == "pgp-signature" ||
979 subType
.toLower() == "pkcs7-mime" ||
980 subType
.toLower() == "pkcs7-signature" ||
981 subType
.toLower() == "x-pkcs7-signature" ||
982 ( subType
.toLower() == "octet-stream" &&
983 fileName
.toLower() == "msg.asc" ) ) );
986 QString
formatString( const QString
&wildString
, const QString
&fromAddr
)
990 if ( wildString
.isEmpty() ) {
994 unsigned int strLength( wildString
.length() );
995 for ( uint i
=0; i
<strLength
; ) {
996 QChar ch
= wildString
[i
++];
997 if ( ch
== '%' && i
<strLength
) {
998 ch
= wildString
[i
++];
999 switch ( ch
.toLatin1() ) {
1000 case 'f': // sender's initals
1002 QString str
= stripEmailAddr( fromAddr
);
1005 for ( ; str
[j
]>' '; j
++ )
1007 unsigned int strLength( str
.length() );
1008 for ( ; j
< strLength
&& str
[j
] <= ' '; j
++ )
1011 if ( str
[j
] > ' ' ) {
1014 if ( str
[1] > ' ' ) {
1038 QString
cleanFileName( const QString
&name
)
1040 QString fileName
= name
.trimmed();
1042 // We need to replace colons with underscores since those cause problems with
1043 // KFileDialog (bug in KFileDialog though) and also on Windows filesystems.
1044 // We also look at the special case of ": ", since converting that to "_ "
1045 // would look strange, simply "_" looks better.
1046 // https://issues.kolab.org/issue3805
1047 fileName
.replace( ": ", "_" );
1048 // replace all ':' with '_' because ':' isn't allowed on FAT volumes
1049 fileName
.replace( ':', '_' );
1050 // better not use a dir-delimiter in a filename
1051 fileName
.replace( '/', '_' );
1052 fileName
.replace( '\\', '_' );
1054 // replace all '.' with '_', not just at the start of the filename
1055 // but don't replace the last '.' before the file extension.
1056 int i
= fileName
.lastIndexOf( '.' );
1058 i
= fileName
.lastIndexOf( '.' , i
- 1 );
1062 fileName
.replace( i
, 1, '_' );
1063 i
= fileName
.lastIndexOf( '.', i
- 1 );
1066 // replace all '~' with '_', not just leading '~' either.
1067 fileName
.replace( '~', '_' );