1 /* (Text)Component - message component base class and plain text
3 ** Copyright 2001 Dr. Zoidberg Enterprises. All rights reserved.
14 class _EXPORT BMailComponent
;
15 class _EXPORT BTextMailComponent
;
17 #include <MailComponent.h>
18 #include <MailAttachment.h>
19 #include <MailContainer.h>
20 #include <mail_util.h>
22 #include <CharacterSet.h>
23 #include <CharacterSetRoster.h>
25 using namespace BPrivate
;
27 struct CharsetConversionEntry
33 extern const CharsetConversionEntry mail_charsets
[];
36 const char* kHeaderCharsetString
= "header-charset";
37 const char* kHeaderEncodingString
= "header-encoding";
38 // Special field names in the headers which specify the character set (int32)
39 // and encoding (int8) to use when converting the headers from UTF-8 to the
40 // output e-mail format (rfc2047). Since they are numbers, not strings, the
41 // extra fields won't be output.
44 BMailComponent::BMailComponent(uint32 defaultCharSet
)
45 : _charSetForTextDecoding (defaultCharSet
)
50 BMailComponent::~BMailComponent()
56 BMailComponent::ComponentType()
58 if (NULL
!= dynamic_cast<BAttributedMailAttachment
*> (this))
59 return B_MAIL_ATTRIBUTED_ATTACHMENT
;
61 BMimeType type
, super
;
63 type
.GetSupertype(&super
);
65 //---------ATT-This code *desperately* needs to be improved
66 if (super
== "multipart") {
67 if (type
== "multipart/x-bfile") // Not likely, they have the MIME
68 return B_MAIL_ATTRIBUTED_ATTACHMENT
; // of their data contents.
70 return B_MAIL_MULTIPART_CONTAINER
;
71 } else if (!IsAttachment() && (super
== "text" || type
.Type() == NULL
))
72 return B_MAIL_PLAIN_TEXT_BODY
;
74 return B_MAIL_SIMPLE_ATTACHMENT
;
79 BMailComponent::WhatIsThis()
81 switch (ComponentType()) {
82 case B_MAIL_SIMPLE_ATTACHMENT
:
83 return new BSimpleMailAttachment
;
84 case B_MAIL_ATTRIBUTED_ATTACHMENT
:
85 return new BAttributedMailAttachment
;
86 case B_MAIL_MULTIPART_CONTAINER
:
87 return new BMIMEMultipartMailContainer (NULL
, NULL
, _charSetForTextDecoding
);
88 case B_MAIL_PLAIN_TEXT_BODY
:
90 return new BTextMailComponent (NULL
, _charSetForTextDecoding
);
96 BMailComponent::IsAttachment()
98 const char* disposition
= HeaderField("Content-Disposition");
99 if ((disposition
!= NULL
)
100 && (strncasecmp(disposition
, "Attachment", strlen("Attachment")) == 0))
104 HeaderField("Content-Type", &header
);
105 if (header
.HasString("name"))
108 if (HeaderField("Content-Location", &header
) == B_OK
)
113 if (type
== "multipart/x-bfile")
121 BMailComponent::SetHeaderField(const char* key
, const char* value
,
122 uint32 charset
, mail_encoding encoding
, bool replace_existing
)
124 if (replace_existing
)
125 headers
.RemoveName(key
);
126 if (value
!= NULL
&& value
[0] != 0) // Empty or NULL strings mean delete header.
127 headers
.AddString(key
, value
);
129 // Latest setting of the character set and encoding to use when outputting
130 // the headers is the one which affects all the headers. There used to be
131 // separate settings for each item in the headers, but it never actually
132 // worked (can't store multiple items of different types in a BMessage).
133 if (charset
!= B_MAIL_NULL_CONVERSION
134 && headers
.ReplaceInt32 (kHeaderCharsetString
, charset
) != B_OK
)
135 headers
.AddInt32(kHeaderCharsetString
, charset
);
136 if (encoding
!= null_encoding
137 && headers
.ReplaceInt8 (kHeaderEncodingString
, encoding
) != B_OK
)
138 headers
.AddInt8(kHeaderEncodingString
, encoding
);
143 BMailComponent::SetHeaderField(const char* key
, BMessage
* structure
,
144 bool replace_existing
)
146 int32 charset
= B_MAIL_NULL_CONVERSION
;
147 int8 encoding
= null_encoding
;
148 const char* unlabeled
= "unlabeled";
150 if (replace_existing
)
151 headers
.RemoveName(key
);
154 if (structure
->HasString(unlabeled
))
155 value
<< structure
->FindString(unlabeled
) << "; ";
160 for (int32 i
= 0; structure
->GetInfo(B_STRING_TYPE
, i
,
161 #if !defined(HAIKU_TARGET_PLATFORM_DANO)
164 &name
, &type
) == B_OK
; i
++) {
166 if (strcasecmp(name
, unlabeled
) == 0)
169 structure
->FindString(name
, &sub_val
);
170 value
<< name
<< '=';
171 if (BString(sub_val
).FindFirst(' ') > 0)
172 value
<< '\"' << sub_val
<< "\"; ";
174 value
<< sub_val
<< "; ";
177 value
.Truncate(value
.Length() - 2); //-----Remove the last "; "
179 if (structure
->HasInt32(kHeaderCharsetString
))
180 structure
->FindInt32(kHeaderCharsetString
, &charset
);
181 if (structure
->HasInt8(kHeaderEncodingString
))
182 structure
->FindInt8(kHeaderEncodingString
, &encoding
);
184 SetHeaderField(key
, value
.String(), (uint32
) charset
, (mail_encoding
) encoding
);
189 BMailComponent::HeaderField(const char* key
, int32 index
) const
191 const char* string
= NULL
;
193 headers
.FindString(key
, index
, &string
);
199 BMailComponent::HeaderField(const char* key
, BMessage
* structure
,
202 BString string
= HeaderField(key
, index
);
204 return B_NAME_NOT_FOUND
;
211 // Break the header into parts, they're separated by semicolons, like this:
212 // Content-Type: multipart/mixed;boundary= "----=_NextPart_000_00AA_354DB459.5977A1CA"
213 // There's also white space and quotes to be removed, and even comments in
214 // parenthesis like this, which can appear anywhere white space is: (header comment)
216 while (end
< string
.Length()) {
217 end
= string
.FindFirst(';', i
);
219 end
= string
.Length();
221 string
.CopyInto(sub_cat
, i
, end
- i
);
224 //-------Trim spaces off of beginning and end of text
225 for (int32 h
= 0; h
< sub_cat
.Length(); h
++) {
226 if (!isspace(sub_cat
.ByteAt(h
))) {
227 sub_cat
.Remove(0, h
);
231 for (int32 h
= sub_cat
.Length() - 1; h
>= 0; h
--) {
232 if (!isspace(sub_cat
.ByteAt(h
))) {
233 sub_cat
.Truncate(h
+ 1);
237 //--------Split along '='
238 int32 first_equal
= sub_cat
.FindFirst('=');
239 if (first_equal
>= 0) {
240 sub_cat
.CopyInto(end_piece
, first_equal
+ 1, sub_cat
.Length() - first_equal
- 1);
241 sub_cat
.Truncate(first_equal
);
242 // Remove leading spaces from part after the equals sign.
243 while (isspace (end_piece
.ByteAt(0)))
244 end_piece
.Remove (0 /* index */, 1 /* number of chars */);
245 // Remove quote marks.
246 if (end_piece
.ByteAt(0) == '\"') {
247 end_piece
.Remove(0, 1);
248 end_piece
.Truncate(end_piece
.Length() - 1);
251 structure
->AddString(sub_cat
.String(), end_piece
.String());
253 structure
->AddString("unlabeled", sub_cat
.String());
262 BMailComponent::RemoveHeader(const char* key
)
264 return headers
.RemoveName(key
);
269 BMailComponent::HeaderAt(int32 index
) const
271 #if defined(HAIKU_TARGET_PLATFORM_DANO)
277 headers
.GetInfo(B_STRING_TYPE
, index
, &name
, &type
);
283 BMailComponent::GetDecodedData(BPositionIO
*)
290 BMailComponent::SetDecodedData(BPositionIO
*)
297 BMailComponent::SetToRFC822(BPositionIO
* data
, size_t /*length*/, bool /*parse_now*/)
301 // Only parse the header here
302 return parse_header(headers
, *data
);
307 BMailComponent::RenderToRFC822(BPositionIO
* render_to
)
309 int32 charset
= B_ISO15_CONVERSION
;
310 int8 encoding
= quoted_printable
;
314 ssize_t amountWritten
;
316 type_code stupidity_personified
= B_STRING_TYPE
;
319 if (headers
.HasInt32(kHeaderCharsetString
))
320 headers
.FindInt32(kHeaderCharsetString
, &charset
);
321 if (headers
.HasInt8(kHeaderEncodingString
))
322 headers
.FindInt8(kHeaderEncodingString
, &encoding
);
324 for (int32 index
= 0; headers
.GetInfo(B_STRING_TYPE
, index
,
325 #if !defined(HAIKU_TARGET_PLATFORM_DANO)
328 &key
, &stupidity_personified
, &count
) == B_OK
; index
++) {
329 for (int32 g
= 0; g
< count
; g
++) {
330 headers
.FindString(key
, g
, (const char**)&value
);
331 allocd
= (char*)malloc(strlen(value
) + 1);
332 strcpy(allocd
, value
);
334 concat
<< key
<< ": ";
335 concat
.CapitalizeEachWord();
337 concat
.Append(allocd
, utf8_to_rfc2047(&allocd
, strlen(value
),
340 FoldLineAtWhiteSpaceAndAddCRLF(concat
);
342 amountWritten
= render_to
->Write(concat
.String(), concat
.Length());
343 if (amountWritten
< 0)
344 return amountWritten
; // IO error happened, usually disk full.
349 render_to
->Write("\r\n", 2);
356 BMailComponent::MIMEType(BMimeType
* mime
)
358 bool foundBestHeader
;
359 const char* boundaryString
;
362 const char* typeAsString
= NULL
;
363 char typeAsLowerCaseString
[B_MIME_TYPE_LENGTH
];
365 // Find the best Content-Type header to use. There should really be just
366 // one, but evil spammers sneakily insert one for multipart (with no
367 // boundary string), then one for text/plain. We'll scan through them and
368 // only use the multipart one if there are no others, and it has a
371 foundBestHeader
= false;
372 for (i
= 0; msg
.MakeEmpty(), HeaderField("Content-Type", &msg
, i
) == B_OK
; i
++) {
373 typeAsString
= msg
.FindString("unlabeled");
374 if (typeAsString
!= NULL
&& strncasecmp(typeAsString
, "multipart", 9) != 0) {
375 foundBestHeader
= true;
379 if (!foundBestHeader
) {
380 for (i
= 0; msg
.MakeEmpty(), HeaderField("Content-Type", &msg
, i
) == B_OK
; i
++) {
381 typeAsString
= msg
.FindString("unlabeled");
382 if (typeAsString
!= NULL
&& strncasecmp(typeAsString
, "multipart", 9) == 0) {
383 boundaryString
= msg
.FindString("boundary");
384 if (boundaryString
!= NULL
&& strlen(boundaryString
) > 0) {
385 foundBestHeader
= true;
391 // At this point we have the good MIME type in typeAsString, but only if
392 // foundBestHeader is true.
394 if (!foundBestHeader
) {
395 strcpy(typeAsLowerCaseString
, "text/plain"); // Hope this is an OK default.
397 // Some extra processing to convert mixed or upper case MIME types into
398 // lower case, since the BeOS R5 BMimeType is case sensitive (but OpenBeOS
399 // isn't). Also truncate the string if it is too long.
400 for (i
= 0; i
< sizeof(typeAsLowerCaseString
) - 1
401 && typeAsString
[i
] != 0; i
++)
402 typeAsLowerCaseString
[i
] = tolower(typeAsString
[i
]);
403 typeAsLowerCaseString
[i
] = 0;
405 // Some old e-mail programs saved the type as just "TEXT", which we need to
406 // convert to "text/plain" since the rest of the code looks for that.
407 if (strcmp(typeAsLowerCaseString
, "text") == 0)
408 strcpy(typeAsLowerCaseString
, "text/plain");
410 mime
->SetTo(typeAsLowerCaseString
);
415 void BMailComponent::_ReservedComponent1() {}
416 void BMailComponent::_ReservedComponent2() {}
417 void BMailComponent::_ReservedComponent3() {}
418 void BMailComponent::_ReservedComponent4() {}
419 void BMailComponent::_ReservedComponent5() {}
422 //-------------------------------------------------------------------------
426 BTextMailComponent::BTextMailComponent(const char* text
, uint32 defaultCharSet
)
427 : BMailComponent(defaultCharSet
),
428 encoding(quoted_printable
),
429 charset(B_ISO15_CONVERSION
),
435 SetHeaderField("MIME-Version", "1.0");
439 BTextMailComponent::~BTextMailComponent()
445 BTextMailComponent::SetEncoding(mail_encoding encoding
, int32 charset
)
447 this->encoding
= encoding
;
448 this->charset
= charset
;
453 BTextMailComponent::SetText(const char* text
)
455 this->text
.SetTo(text
);
462 BTextMailComponent::AppendText(const char* text
)
471 BTextMailComponent::Text()
475 return text
.String();
480 BTextMailComponent::BStringText()
489 BTextMailComponent::Quote(const char* message
, const char* quote_style
)
494 string
<< '\n' << quote_style
;
495 text
.ReplaceAll("\n",string
.String());
499 text
.Prepend(string
.String());
504 BTextMailComponent::GetDecodedData(BPositionIO
* data
)
512 BMimeType
textAny("text");
514 if (MIMEType(&type
) == B_OK
&& textAny
.Contains(&type
))
515 // Write out the string which has been both decoded from quoted
516 // printable or base64 etc, and then converted to UTF-8 from whatever
517 // character set the message specified. Do it for text/html,
518 // text/plain and all other text datatypes. Of course, if the message
519 // is HTML and specifies a META tag for a character set, it will now be
520 // wrong. But then we don't display HTML in BeMail, yet.
521 written
= data
->Write(text
.String(), text
.Length());
523 // Just write out whatever the binary contents are, only decoded from
524 // the quoted printable etc format.
525 written
= data
->Write(decoded
.String(), decoded
.Length());
527 return written
>= 0 ? B_OK
: written
;
532 BTextMailComponent::SetDecodedData(BPositionIO
* data
)
537 while ((buf_len
= data
->Read(buffer
, 254)) > 0) {
539 this->text
<< buffer
;
549 BTextMailComponent::SetToRFC822(BPositionIO
* data
, size_t length
, bool parseNow
)
551 off_t position
= data
->Position();
552 BMailComponent::SetToRFC822(data
, length
);
554 // Some malformed MIME headers can have the header running into the
555 // boundary of the next MIME chunk, resulting in a negative length.
556 length
-= data
->Position() - position
;
557 if ((ssize_t
) length
< 0)
562 raw_offset
= data
->Position();
565 // copies the data stream and sets the raw_data variable to NULL
574 BTextMailComponent::ParseRaw()
576 if (raw_data
== NULL
)
579 raw_data
->Seek(raw_offset
, SEEK_SET
);
581 BMessage content_type
;
582 HeaderField("Content-Type", &content_type
);
584 charset
= _charSetForTextDecoding
;
585 if (charset
== B_MAIL_NULL_CONVERSION
&& content_type
.HasString("charset")) {
586 const char* charset_string
= content_type
.FindString("charset");
587 if (strcasecmp(charset_string
, "us-ascii") == 0) {
588 charset
= B_MAIL_US_ASCII_CONVERSION
;
589 } else if (strcasecmp(charset_string
, "utf-8") == 0) {
590 charset
= B_MAIL_UTF8_CONVERSION
;
592 const BCharacterSet
* cs
= BCharacterSetRoster::FindCharacterSetByName(charset_string
);
594 charset
= cs
->GetConversionID();
599 encoding
= encoding_for_cte(HeaderField("Content-Transfer-Encoding"));
601 char* buffer
= (char*)malloc(raw_length
+ 1);
606 if ((bytes
= raw_data
->Read(buffer
, raw_length
)) < 0)
609 char* string
= decoded
.LockBuffer(bytes
+ 1);
610 bytes
= decode(encoding
, string
, buffer
, bytes
, 0);
614 // Change line ends from \r\n to just \n. Though this won't work properly
615 // for UTF-16 because \r takes up two bytes rather than one.
618 char* end
= string
+ bytes
;
619 for (dest
= src
= string
; src
< end
; src
++) {
623 decoded
.UnlockBuffer(dest
- string
);
624 bytes
= decoded
.Length(); // Might have shrunk a bit.
626 // If the character set wasn't specified, try to guess. ISO-2022-JP
627 // contains the escape sequences ESC $ B or ESC $ @ to turn on 2 byte
628 // Japanese, and ESC ( J to switch to Roman, or sometimes ESC ( B for
629 // ASCII. We'll just try looking for the two switch to Japanese sequences.
631 if (charset
== B_MAIL_NULL_CONVERSION
) {
632 if (decoded
.FindFirst ("\e$B") >= 0 || decoded
.FindFirst ("\e$@") >= 0)
633 charset
= B_JIS_CONVERSION
;
634 else // Just assume the usual Latin-9 character set.
635 charset
= B_ISO15_CONVERSION
;
639 int32 destLength
= bytes
* 3 /* in case it grows */ + 1 /* +1 so it isn't zero which crashes */;
640 string
= text
.LockBuffer(destLength
);
641 mail_convert_to_utf8(charset
, decoded
.String(), &bytes
, string
,
642 &destLength
, &state
);
644 text
.UnlockBuffer(destLength
);
646 text
.UnlockBuffer(0);
656 BTextMailComponent::RenderToRFC822(BPositionIO
* render_to
)
658 status_t status
= ParseRaw();
664 BString content_type
;
665 content_type
<< type
.Type(); // Preserve MIME type (e.g. text/html
667 for (uint32 i
= 0; mail_charsets
[i
].charset
!= NULL
; i
++) {
668 if (mail_charsets
[i
].flavor
== charset
) {
669 content_type
<< "; charset=\"" << mail_charsets
[i
].charset
<< "\"";
674 SetHeaderField("Content-Type", content_type
.String());
676 const char* transfer_encoding
= NULL
;
679 transfer_encoding
= "base64";
681 case quoted_printable
:
682 transfer_encoding
= "quoted-printable";
685 transfer_encoding
= "8bit";
689 transfer_encoding
= "7bit";
693 SetHeaderField("Content-Transfer-Encoding", transfer_encoding
);
695 BMailComponent::RenderToRFC822(render_to
);
697 BString modified
= this->text
;
700 int32 len
= this->text
.Length();
702 int32 dest_len
= len
* 5;
703 // Shift-JIS can have a 3 byte escape sequence and a 2 byte code for
704 // each character (which could just be 2 bytes in UTF-8, or even 1 byte
705 // if it's regular ASCII), so it can get quite a bit larger than the
706 // original text. Multiplying by 5 should make more than enough space.
707 char* raw
= alt
.LockBuffer(dest_len
);
709 mail_convert_from_utf8(charset
, this->text
.String(), &len
, raw
,
711 alt
.UnlockBuffer(dest_len
);
713 raw
= modified
.LockBuffer((alt
.Length() * 3) + 1);
716 len
= encode_base64(raw
, alt
.String(), alt
.Length(), false);
719 case quoted_printable
:
720 len
= encode_qp(raw
, alt
.String(), alt
.Length(), false);
727 strcpy(raw
, alt
.String());
729 modified
.UnlockBuffer(len
);
731 if (encoding
!= base64
) // encode_base64 already does CRLF line endings.
732 modified
.ReplaceAll("\n","\r\n");
734 // There seem to be a possibility of NULL bytes in the text, so lets
735 // filter them out, shouldn't be any after the encoding stage.
737 char* string
= modified
.LockBuffer(modified
.Length());
738 for (int32 i
= modified
.Length(); i
-- > 0;) {
739 if (string
[i
] != '\0')
742 puts("BTextMailComponent::RenderToRFC822: NULL byte in text!!");
745 modified
.UnlockBuffer();
747 // word wrapping is already done by BeMail (user-configurable)
748 // and it does it *MUCH* nicer.
750 // //------Desperate bid to wrap lines
751 // int32 curr_line_length = 0;
752 // int32 last_space = 0;
754 // for (int32 i = 0; i < modified.Length(); i++) {
755 // if (isspace(modified.ByteAt(i)))
758 // if ((modified.ByteAt(i) == '\r') && (modified.ByteAt(i+1) == '\n'))
759 // curr_line_length = 0;
761 // curr_line_length++;
763 // if (curr_line_length > 80) {
764 // if (last_space >= 0) {
765 // modified.Insert("\r\n",last_space);
767 // curr_line_length = 0;
774 render_to
->Write(modified
.String(), modified
.Length());
780 void BTextMailComponent::_ReservedText1() {}
781 void BTextMailComponent::_ReservedText2() {}