BPicture: Fix archive constructor.
[haiku.git] / src / kits / mail / MailComponent.cpp
blob5c5dcbc27a015ec64802dcf53bdac61b17984fb7
1 /* (Text)Component - message component base class and plain text
2 **
3 ** Copyright 2001 Dr. Zoidberg Enterprises. All rights reserved.
4 */
7 #include <String.h>
8 #include <Mime.h>
10 #include <ctype.h>
11 #include <stdlib.h>
12 #include <strings.h>
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
29 const char* charset;
30 uint32 flavor;
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()
55 uint32
56 BMailComponent::ComponentType()
58 if (NULL != dynamic_cast<BAttributedMailAttachment*> (this))
59 return B_MAIL_ATTRIBUTED_ATTACHMENT;
61 BMimeType type, super;
62 MIMEType(&type);
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.
69 else
70 return B_MAIL_MULTIPART_CONTAINER;
71 } else if (!IsAttachment() && (super == "text" || type.Type() == NULL))
72 return B_MAIL_PLAIN_TEXT_BODY;
73 else
74 return B_MAIL_SIMPLE_ATTACHMENT;
78 BMailComponent*
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:
89 default:
90 return new BTextMailComponent (NULL, _charSetForTextDecoding);
95 bool
96 BMailComponent::IsAttachment()
98 const char* disposition = HeaderField("Content-Disposition");
99 if ((disposition != NULL)
100 && (strncasecmp(disposition, "Attachment", strlen("Attachment")) == 0))
101 return true;
103 BMessage header;
104 HeaderField("Content-Type", &header);
105 if (header.HasString("name"))
106 return true;
108 if (HeaderField("Content-Location", &header) == B_OK)
109 return true;
111 BMimeType type;
112 MIMEType(&type);
113 if (type == "multipart/x-bfile")
114 return true;
116 return false;
120 void
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);
142 void
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);
153 BString value;
154 if (structure->HasString(unlabeled))
155 value << structure->FindString(unlabeled) << "; ";
157 const char* name;
158 const char* sub_val;
159 type_code type;
160 for (int32 i = 0; structure->GetInfo(B_STRING_TYPE, i,
161 #if !defined(HAIKU_TARGET_PLATFORM_DANO)
162 (char**)
163 #endif
164 &name, &type) == B_OK; i++) {
166 if (strcasecmp(name, unlabeled) == 0)
167 continue;
169 structure->FindString(name, &sub_val);
170 value << name << '=';
171 if (BString(sub_val).FindFirst(' ') > 0)
172 value << '\"' << sub_val << "\"; ";
173 else
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);
188 const char*
189 BMailComponent::HeaderField(const char* key, int32 index) const
191 const char* string = NULL;
193 headers.FindString(key, index, &string);
194 return string;
198 status_t
199 BMailComponent::HeaderField(const char* key, BMessage* structure,
200 int32 index) const
202 BString string = HeaderField(key, index);
203 if (string == "")
204 return B_NAME_NOT_FOUND;
206 BString sub_cat;
207 BString end_piece;
208 int32 i = 0;
209 int32 end = 0;
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);
218 if (end < 0)
219 end = string.Length();
221 string.CopyInto(sub_cat, i, end - i);
222 i = end + 1;
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);
228 break;
231 for (int32 h = sub_cat.Length() - 1; h >= 0; h--) {
232 if (!isspace(sub_cat.ByteAt(h))) {
233 sub_cat.Truncate(h + 1);
234 break;
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);
250 sub_cat.ToLower();
251 structure->AddString(sub_cat.String(), end_piece.String());
252 } else {
253 structure->AddString("unlabeled", sub_cat.String());
257 return B_OK;
261 status_t
262 BMailComponent::RemoveHeader(const char* key)
264 return headers.RemoveName(key);
268 const char*
269 BMailComponent::HeaderAt(int32 index) const
271 #if defined(HAIKU_TARGET_PLATFORM_DANO)
272 const
273 #endif
274 char* name = NULL;
275 type_code type;
277 headers.GetInfo(B_STRING_TYPE, index, &name, &type);
278 return name;
282 status_t
283 BMailComponent::GetDecodedData(BPositionIO*)
285 return B_OK;
289 status_t
290 BMailComponent::SetDecodedData(BPositionIO*)
292 return B_OK;
296 status_t
297 BMailComponent::SetToRFC822(BPositionIO* data, size_t /*length*/, bool /*parse_now*/)
299 headers.MakeEmpty();
301 // Only parse the header here
302 return parse_header(headers, *data);
306 status_t
307 BMailComponent::RenderToRFC822(BPositionIO* render_to)
309 int32 charset = B_ISO15_CONVERSION;
310 int8 encoding = quoted_printable;
311 const char* key;
312 const char* value;
313 char* allocd;
314 ssize_t amountWritten;
315 BString concat;
316 type_code stupidity_personified = B_STRING_TYPE;
317 int32 count = 0;
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)
326 (char**)
327 #endif
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),
338 charset, encoding));
339 free(allocd);
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.
345 concat = "";
349 render_to->Write("\r\n", 2);
351 return B_OK;
355 status_t
356 BMailComponent::MIMEType(BMimeType* mime)
358 bool foundBestHeader;
359 const char* boundaryString;
360 unsigned int i;
361 BMessage msg;
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
369 // boundary.
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;
376 break;
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;
386 break;
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.
396 } else {
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);
411 return B_OK;
415 void BMailComponent::_ReservedComponent1() {}
416 void BMailComponent::_ReservedComponent2() {}
417 void BMailComponent::_ReservedComponent3() {}
418 void BMailComponent::_ReservedComponent4() {}
419 void BMailComponent::_ReservedComponent5() {}
422 //-------------------------------------------------------------------------
423 // #pragma mark -
426 BTextMailComponent::BTextMailComponent(const char* text, uint32 defaultCharSet)
427 : BMailComponent(defaultCharSet),
428 encoding(quoted_printable),
429 charset(B_ISO15_CONVERSION),
430 raw_data(NULL)
432 if (text != NULL)
433 SetText(text);
435 SetHeaderField("MIME-Version", "1.0");
439 BTextMailComponent::~BTextMailComponent()
444 void
445 BTextMailComponent::SetEncoding(mail_encoding encoding, int32 charset)
447 this->encoding = encoding;
448 this->charset = charset;
452 void
453 BTextMailComponent::SetText(const char* text)
455 this->text.SetTo(text);
457 raw_data = NULL;
461 void
462 BTextMailComponent::AppendText(const char* text)
464 ParseRaw();
466 this->text << text;
470 const char*
471 BTextMailComponent::Text()
473 ParseRaw();
475 return text.String();
479 BString*
480 BTextMailComponent::BStringText()
482 ParseRaw();
484 return &text;
488 void
489 BTextMailComponent::Quote(const char* message, const char* quote_style)
491 ParseRaw();
493 BString string;
494 string << '\n' << quote_style;
495 text.ReplaceAll("\n",string.String());
497 string = message;
498 string << '\n';
499 text.Prepend(string.String());
503 status_t
504 BTextMailComponent::GetDecodedData(BPositionIO* data)
506 ParseRaw();
508 if (data == NULL)
509 return B_IO_ERROR;
511 BMimeType type;
512 BMimeType textAny("text");
513 ssize_t written;
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());
522 else
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;
531 status_t
532 BTextMailComponent::SetDecodedData(BPositionIO* data)
534 char buffer[255];
535 size_t buf_len;
537 while ((buf_len = data->Read(buffer, 254)) > 0) {
538 buffer[buf_len] = 0;
539 this->text << buffer;
542 raw_data = NULL;
544 return B_OK;
548 status_t
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)
558 length = 0;
560 raw_data = data;
561 raw_length = length;
562 raw_offset = data->Position();
564 if (parseNow) {
565 // copies the data stream and sets the raw_data variable to NULL
566 return ParseRaw();
569 return B_OK;
573 status_t
574 BTextMailComponent::ParseRaw()
576 if (raw_data == NULL)
577 return B_OK;
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;
591 } else {
592 const BCharacterSet* cs = BCharacterSetRoster::FindCharacterSetByName(charset_string);
593 if (cs != NULL) {
594 charset = cs->GetConversionID();
599 encoding = encoding_for_cte(HeaderField("Content-Transfer-Encoding"));
601 char* buffer = (char*)malloc(raw_length + 1);
602 if (buffer == NULL)
603 return B_NO_MEMORY;
605 int32 bytes;
606 if ((bytes = raw_data->Read(buffer, raw_length)) < 0)
607 return B_IO_ERROR;
609 char* string = decoded.LockBuffer(bytes + 1);
610 bytes = decode(encoding, string, buffer, bytes, 0);
611 free(buffer);
612 buffer = NULL;
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.
616 char* dest;
617 char* src;
618 char* end = string + bytes;
619 for (dest = src = string; src < end; src++) {
620 if (*src != '\r')
621 *dest++ = *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;
638 int32 state = 0;
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);
643 if (destLength > 0)
644 text.UnlockBuffer(destLength);
645 else {
646 text.UnlockBuffer(0);
647 text.SetTo(decoded);
650 raw_data = NULL;
651 return B_OK;
655 status_t
656 BTextMailComponent::RenderToRFC822(BPositionIO* render_to)
658 status_t status = ParseRaw();
659 if (status < B_OK)
660 return status;
662 BMimeType type;
663 MIMEType(&type);
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 << "\"";
670 break;
674 SetHeaderField("Content-Type", content_type.String());
676 const char* transfer_encoding = NULL;
677 switch (encoding) {
678 case base64:
679 transfer_encoding = "base64";
680 break;
681 case quoted_printable:
682 transfer_encoding = "quoted-printable";
683 break;
684 case eight_bit:
685 transfer_encoding = "8bit";
686 break;
687 case seven_bit:
688 default:
689 transfer_encoding = "7bit";
690 break;
693 SetHeaderField("Content-Transfer-Encoding", transfer_encoding);
695 BMailComponent::RenderToRFC822(render_to);
697 BString modified = this->text;
698 BString alt;
700 int32 len = this->text.Length();
701 if (len > 0) {
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);
708 int32 state = 0;
709 mail_convert_from_utf8(charset, this->text.String(), &len, raw,
710 &dest_len, &state);
711 alt.UnlockBuffer(dest_len);
713 raw = modified.LockBuffer((alt.Length() * 3) + 1);
714 switch (encoding) {
715 case base64:
716 len = encode_base64(raw, alt.String(), alt.Length(), false);
717 raw[len] = 0;
718 break;
719 case quoted_printable:
720 len = encode_qp(raw, alt.String(), alt.Length(), false);
721 raw[len] = 0;
722 break;
723 case eight_bit:
724 case seven_bit:
725 default:
726 len = alt.Length();
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')
740 continue;
742 puts("BTextMailComponent::RenderToRFC822: NULL byte in text!!");
743 string[i] = ' ';
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)))
756 // last_space = i;
758 // if ((modified.ByteAt(i) == '\r') && (modified.ByteAt(i+1) == '\n'))
759 // curr_line_length = 0;
760 // else
761 // curr_line_length++;
763 // if (curr_line_length > 80) {
764 // if (last_space >= 0) {
765 // modified.Insert("\r\n",last_space);
766 // last_space = -1;
767 // curr_line_length = 0;
768 // }
769 // }
770 // }
772 modified << "\r\n";
774 render_to->Write(modified.String(), modified.Length());
776 return B_OK;
780 void BTextMailComponent::_ReservedText1() {}
781 void BTextMailComponent::_ReservedText2() {}