1 /* (Text)Component - message component base class and plain text 2 ** 3 ** Copyright 2001 Dr. Zoidberg Enterprises. All rights reserved. 4 */ 5 6 7 #include <String.h> 8 #include <Mime.h> 9 10 #include <ctype.h> 11 #include <stdlib.h> 12 13 class _EXPORT BMailComponent; 14 class _EXPORT BTextMailComponent; 15 16 #include <MailComponent.h> 17 #include <MailAttachment.h> 18 #include <MailContainer.h> 19 #include <mail_util.h> 20 21 #include <CharacterSet.h> 22 #include <CharacterSetRoster.h> 23 24 using namespace BPrivate ; 25 26 struct CharsetConversionEntry 27 { 28 const char* charset; 29 uint32 flavor; 30 }; 31 32 extern const CharsetConversionEntry mail_charsets[]; 33 34 35 const char* kHeaderCharsetString = "header-charset"; 36 const char* kHeaderEncodingString = "header-encoding"; 37 // Special field names in the headers which specify the character set (int32) 38 // and encoding (int8) to use when converting the headers from UTF-8 to the 39 // output e-mail format (rfc2047). Since they are numbers, not strings, the 40 // extra fields won't be output. 41 42 43 BMailComponent::BMailComponent(uint32 defaultCharSet) 44 : _charSetForTextDecoding (defaultCharSet) 45 { 46 } 47 48 49 BMailComponent::~BMailComponent() 50 { 51 } 52 53 54 uint32 55 BMailComponent::ComponentType() 56 { 57 if (NULL != dynamic_cast<BAttributedMailAttachment*> (this)) 58 return B_MAIL_ATTRIBUTED_ATTACHMENT; 59 60 BMimeType type, super; 61 MIMEType(&type); 62 type.GetSupertype(&super); 63 64 //---------ATT-This code *desperately* needs to be improved 65 if (super == "multipart") { 66 if (type == "multipart/x-bfile") // Not likely, they have the MIME 67 return B_MAIL_ATTRIBUTED_ATTACHMENT; // of their data contents. 68 else 69 return B_MAIL_MULTIPART_CONTAINER; 70 } else if (!IsAttachment() && (super == "text" || type.Type() == NULL)) 71 return B_MAIL_PLAIN_TEXT_BODY; 72 else 73 return B_MAIL_SIMPLE_ATTACHMENT; 74 } 75 76 77 BMailComponent* 78 BMailComponent::WhatIsThis() 79 { 80 switch (ComponentType()) { 81 case B_MAIL_SIMPLE_ATTACHMENT: 82 return new BSimpleMailAttachment; 83 case B_MAIL_ATTRIBUTED_ATTACHMENT: 84 return new BAttributedMailAttachment; 85 case B_MAIL_MULTIPART_CONTAINER: 86 return new BMIMEMultipartMailContainer (NULL, NULL, _charSetForTextDecoding); 87 case B_MAIL_PLAIN_TEXT_BODY: 88 default: 89 return new BTextMailComponent (NULL, _charSetForTextDecoding); 90 } 91 } 92 93 94 bool 95 BMailComponent::IsAttachment() 96 { 97 const char* disposition = HeaderField("Content-Disposition"); 98 if ((disposition != NULL) 99 && (strncasecmp(disposition, "Attachment", strlen("Attachment")) == 0)) 100 return true; 101 102 BMessage header; 103 HeaderField("Content-Type", &header); 104 if (header.HasString("name")) 105 return true; 106 107 if (HeaderField("Content-Location", &header) == B_OK) 108 return true; 109 110 BMimeType type; 111 MIMEType(&type); 112 if (type == "multipart/x-bfile") 113 return true; 114 115 return false; 116 } 117 118 119 void 120 BMailComponent::SetHeaderField(const char* key, const char* value, 121 uint32 charset, mail_encoding encoding, bool replace_existing) 122 { 123 if (replace_existing) 124 headers.RemoveName(key); 125 if (value != NULL && value[0] != 0) // Empty or NULL strings mean delete header. 126 headers.AddString(key, value); 127 128 // Latest setting of the character set and encoding to use when outputting 129 // the headers is the one which affects all the headers. There used to be 130 // separate settings for each item in the headers, but it never actually 131 // worked (can't store multiple items of different types in a BMessage). 132 if (charset != B_MAIL_NULL_CONVERSION 133 && headers.ReplaceInt32 (kHeaderCharsetString, charset) != B_OK) 134 headers.AddInt32(kHeaderCharsetString, charset); 135 if (encoding != null_encoding 136 && headers.ReplaceInt8 (kHeaderEncodingString, encoding) != B_OK) 137 headers.AddInt8(kHeaderEncodingString, encoding); 138 } 139 140 141 void 142 BMailComponent::SetHeaderField(const char* key, BMessage* structure, 143 bool replace_existing) 144 { 145 int32 charset = B_MAIL_NULL_CONVERSION; 146 int8 encoding = null_encoding; 147 const char* unlabeled = "unlabeled"; 148 149 if (replace_existing) 150 headers.RemoveName(key); 151 152 BString value; 153 if (structure->HasString(unlabeled)) 154 value << structure->FindString(unlabeled) << "; "; 155 156 const char* name; 157 const char* sub_val; 158 type_code type; 159 for (int32 i = 0; structure->GetInfo(B_STRING_TYPE, i, 160 #if !defined(HAIKU_TARGET_PLATFORM_DANO) 161 (char**) 162 #endif 163 &name, &type) == B_OK; i++) { 164 165 if (strcasecmp(name, unlabeled) == 0) 166 continue; 167 168 structure->FindString(name, &sub_val); 169 value << name << '='; 170 if (BString(sub_val).FindFirst(' ') > 0) 171 value << '\"' << sub_val << "\"; "; 172 else 173 value << sub_val << "; "; 174 } 175 176 value.Truncate(value.Length() - 2); //-----Remove the last "; " 177 178 if (structure->HasInt32(kHeaderCharsetString)) 179 structure->FindInt32(kHeaderCharsetString, &charset); 180 if (structure->HasInt8(kHeaderEncodingString)) 181 structure->FindInt8(kHeaderEncodingString, &encoding); 182 183 SetHeaderField(key, value.String(), (uint32) charset, (mail_encoding) encoding); 184 } 185 186 187 const char* 188 BMailComponent::HeaderField(const char* key, int32 index) 189 { 190 const char* string = NULL; 191 192 headers.FindString(key, index, &string); 193 return string; 194 } 195 196 197 status_t 198 BMailComponent::HeaderField(const char* key, BMessage* structure, int32 index) 199 { 200 BString string = HeaderField(key, index); 201 if (string == "") 202 return B_NAME_NOT_FOUND; 203 204 BString sub_cat; 205 BString end_piece; 206 int32 i = 0; 207 int32 end = 0; 208 209 // Break the header into parts, they're separated by semicolons, like this: 210 // Content-Type: multipart/mixed;boundary= "----=_NextPart_000_00AA_354DB459.5977A1CA" 211 // There's also white space and quotes to be removed, and even comments in 212 // parenthesis like this, which can appear anywhere white space is: (header comment) 213 214 while (end < string.Length()) { 215 end = string.FindFirst(';', i); 216 if (end < 0) 217 end = string.Length(); 218 219 string.CopyInto(sub_cat, i, end - i); 220 i = end + 1; 221 222 //-------Trim spaces off of beginning and end of text 223 for (int32 h = 0; h < sub_cat.Length(); h++) { 224 if (!isspace(sub_cat.ByteAt(h))) { 225 sub_cat.Remove(0, h); 226 break; 227 } 228 } 229 for (int32 h = sub_cat.Length() - 1; h >= 0; h--) { 230 if (!isspace(sub_cat.ByteAt(h))) { 231 sub_cat.Truncate(h + 1); 232 break; 233 } 234 } 235 //--------Split along '=' 236 int32 first_equal = sub_cat.FindFirst('='); 237 if (first_equal >= 0) { 238 sub_cat.CopyInto(end_piece, first_equal + 1, sub_cat.Length() - first_equal - 1); 239 sub_cat.Truncate(first_equal); 240 // Remove leading spaces from part after the equals sign. 241 while (isspace (end_piece.ByteAt(0))) 242 end_piece.Remove (0 /* index */, 1 /* number of chars */); 243 // Remove quote marks. 244 if (end_piece.ByteAt(0) == '\"') { 245 end_piece.Remove(0, 1); 246 end_piece.Truncate(end_piece.Length() - 1); 247 } 248 sub_cat.ToLower(); 249 structure->AddString(sub_cat.String(), end_piece.String()); 250 } else { 251 structure->AddString("unlabeled", sub_cat.String()); 252 } 253 } 254 255 return B_OK; 256 } 257 258 259 status_t 260 BMailComponent::RemoveHeader(const char* key) 261 { 262 return headers.RemoveName(key); 263 } 264 265 266 const char* 267 BMailComponent::HeaderAt(int32 index) 268 { 269 #if defined(HAIKU_TARGET_PLATFORM_DANO) 270 const 271 #endif 272 char* name = NULL; 273 type_code type; 274 275 headers.GetInfo(B_STRING_TYPE, index, &name, &type); 276 return name; 277 } 278 279 280 status_t 281 BMailComponent::GetDecodedData(BPositionIO*) 282 { 283 return B_OK; 284 } 285 286 287 status_t 288 BMailComponent::SetDecodedData(BPositionIO*) 289 { 290 return B_OK; 291 } 292 293 294 status_t 295 BMailComponent::SetToRFC822(BPositionIO* data, size_t /*length*/, bool /*parse_now*/) 296 { 297 headers.MakeEmpty(); 298 299 // Only parse the header here 300 return parse_header(headers, *data); 301 } 302 303 304 status_t 305 BMailComponent::RenderToRFC822(BPositionIO* render_to) 306 { 307 int32 charset = B_ISO15_CONVERSION; 308 int8 encoding = quoted_printable; 309 const char* key; 310 const char* value; 311 char* allocd; 312 ssize_t amountWritten; 313 BString concat; 314 type_code stupidity_personified = B_STRING_TYPE; 315 int32 count = 0; 316 317 if (headers.HasInt32(kHeaderCharsetString)) 318 headers.FindInt32(kHeaderCharsetString, &charset); 319 if (headers.HasInt8(kHeaderEncodingString)) 320 headers.FindInt8(kHeaderEncodingString, &encoding); 321 322 for (int32 index = 0; headers.GetInfo(B_STRING_TYPE, index, 323 #if !defined(HAIKU_TARGET_PLATFORM_DANO) 324 (char**) 325 #endif 326 &key, &stupidity_personified, &count) == B_OK; index++) { 327 for (int32 g = 0; g < count; g++) { 328 headers.FindString(key, g, (const char**)&value); 329 allocd = (char*)malloc(strlen(value) + 1); 330 strcpy(allocd, value); 331 332 concat << key << ": "; 333 concat.CapitalizeEachWord(); 334 335 concat.Append(allocd, utf8_to_rfc2047(&allocd, strlen(value), 336 charset, encoding)); 337 free(allocd); 338 FoldLineAtWhiteSpaceAndAddCRLF(concat); 339 340 amountWritten = render_to->Write(concat.String(), concat.Length()); 341 if (amountWritten < 0) 342 return amountWritten; // IO error happened, usually disk full. 343 concat = ""; 344 } 345 } 346 347 render_to->Write("\r\n", 2); 348 349 return B_OK; 350 } 351 352 353 status_t 354 BMailComponent::MIMEType(BMimeType* mime) 355 { 356 bool foundBestHeader; 357 const char* boundaryString; 358 unsigned int i; 359 BMessage msg; 360 const char* typeAsString = NULL; 361 char typeAsLowerCaseString[B_MIME_TYPE_LENGTH]; 362 363 // Find the best Content-Type header to use. There should really be just 364 // one, but evil spammers sneakily insert one for multipart (with no 365 // boundary string), then one for text/plain. We'll scan through them and 366 // only use the multipart one if there are no others, and it has a 367 // boundary. 368 369 foundBestHeader = false; 370 for (i = 0; msg.MakeEmpty(), HeaderField("Content-Type", &msg, i) == B_OK; i++) { 371 typeAsString = msg.FindString("unlabeled"); 372 if (typeAsString != NULL && strncasecmp(typeAsString, "multipart", 9) != 0) { 373 foundBestHeader = true; 374 break; 375 } 376 } 377 if (!foundBestHeader) { 378 for (i = 0; msg.MakeEmpty(), HeaderField("Content-Type", &msg, i) == B_OK; i++) { 379 typeAsString = msg.FindString("unlabeled"); 380 if (typeAsString != NULL && strncasecmp(typeAsString, "multipart", 9) == 0) { 381 boundaryString = msg.FindString("boundary"); 382 if (boundaryString != NULL && strlen(boundaryString) > 0) { 383 foundBestHeader = true; 384 break; 385 } 386 } 387 } 388 } 389 // At this point we have the good MIME type in typeAsString, but only if 390 // foundBestHeader is true. 391 392 if (!foundBestHeader) { 393 strcpy(typeAsLowerCaseString, "text/plain"); // Hope this is an OK default. 394 } else { 395 // Some extra processing to convert mixed or upper case MIME types into 396 // lower case, since the BeOS R5 BMimeType is case sensitive (but OpenBeOS 397 // isn't). Also truncate the string if it is too long. 398 for (i = 0; i < sizeof(typeAsLowerCaseString) - 1 399 && typeAsString[i] != 0; i++) 400 typeAsLowerCaseString[i] = tolower(typeAsString[i]); 401 typeAsLowerCaseString[i] = 0; 402 403 // Some old e-mail programs saved the type as just "TEXT", which we need to 404 // convert to "text/plain" since the rest of the code looks for that. 405 if (strcmp(typeAsLowerCaseString, "text") == 0) 406 strcpy(typeAsLowerCaseString, "text/plain"); 407 } 408 mime->SetTo(typeAsLowerCaseString); 409 return B_OK; 410 } 411 412 413 void BMailComponent::_ReservedComponent1() {} 414 void BMailComponent::_ReservedComponent2() {} 415 void BMailComponent::_ReservedComponent3() {} 416 void BMailComponent::_ReservedComponent4() {} 417 void BMailComponent::_ReservedComponent5() {} 418 419 420 //------------------------------------------------------------------------- 421 // #pragma mark - 422 423 424 BTextMailComponent::BTextMailComponent(const char* text, uint32 defaultCharSet) 425 : BMailComponent(defaultCharSet), 426 encoding(quoted_printable), 427 charset(B_ISO15_CONVERSION), 428 raw_data(NULL) 429 { 430 if (text != NULL) 431 SetText(text); 432 433 SetHeaderField("MIME-Version", "1.0"); 434 } 435 436 437 BTextMailComponent::~BTextMailComponent() 438 { 439 } 440 441 442 void 443 BTextMailComponent::SetEncoding(mail_encoding encoding, int32 charset) 444 { 445 this->encoding = encoding; 446 this->charset = charset; 447 } 448 449 450 void 451 BTextMailComponent::SetText(const char* text) 452 { 453 this->text.SetTo(text); 454 455 raw_data = NULL; 456 } 457 458 459 void 460 BTextMailComponent::AppendText(const char* text) 461 { 462 ParseRaw(); 463 464 this->text << text; 465 } 466 467 468 const char* 469 BTextMailComponent::Text() 470 { 471 ParseRaw(); 472 473 return text.String(); 474 } 475 476 477 BString* 478 BTextMailComponent::BStringText() 479 { 480 ParseRaw(); 481 482 return &text; 483 } 484 485 486 void 487 BTextMailComponent::Quote(const char* message, const char* quote_style) 488 { 489 ParseRaw(); 490 491 BString string; 492 string << '\n' << quote_style; 493 text.ReplaceAll("\n",string.String()); 494 495 string = message; 496 string << '\n'; 497 text.Prepend(string.String()); 498 } 499 500 501 status_t 502 BTextMailComponent::GetDecodedData(BPositionIO* data) 503 { 504 ParseRaw(); 505 506 if (data == NULL) 507 return B_IO_ERROR; 508 509 BMimeType type; 510 BMimeType textAny("text"); 511 ssize_t written; 512 if (MIMEType(&type) == B_OK && textAny.Contains(&type)) 513 // Write out the string which has been both decoded from quoted 514 // printable or base64 etc, and then converted to UTF-8 from whatever 515 // character set the message specified. Do it for text/html, 516 // text/plain and all other text datatypes. Of course, if the message 517 // is HTML and specifies a META tag for a character set, it will now be 518 // wrong. But then we don't display HTML in BeMail, yet. 519 written = data->Write(text.String(), text.Length()); 520 else 521 // Just write out whatever the binary contents are, only decoded from 522 // the quoted printable etc format. 523 written = data->Write(decoded.String(), decoded.Length()); 524 525 return written >= 0 ? B_OK : written; 526 } 527 528 529 status_t 530 BTextMailComponent::SetDecodedData(BPositionIO* data) 531 { 532 char buffer[255]; 533 size_t buf_len; 534 535 while ((buf_len = data->Read(buffer, 254)) > 0) { 536 buffer[buf_len] = 0; 537 this->text << buffer; 538 } 539 540 raw_data = NULL; 541 542 return B_OK; 543 } 544 545 546 status_t 547 BTextMailComponent::SetToRFC822(BPositionIO* data, size_t length, bool parseNow) 548 { 549 off_t position = data->Position(); 550 BMailComponent::SetToRFC822(data, length); 551 552 // Some malformed MIME headers can have the header running into the 553 // boundary of the next MIME chunk, resulting in a negative length. 554 length -= data->Position() - position; 555 if ((ssize_t) length < 0) 556 length = 0; 557 558 raw_data = data; 559 raw_length = length; 560 raw_offset = data->Position(); 561 562 if (parseNow) { 563 // copies the data stream and sets the raw_data variable to NULL 564 return ParseRaw(); 565 } 566 567 return B_OK; 568 } 569 570 571 status_t 572 BTextMailComponent::ParseRaw() 573 { 574 if (raw_data == NULL) 575 return B_OK; 576 577 raw_data->Seek(raw_offset, SEEK_SET); 578 579 BMessage content_type; 580 HeaderField("Content-Type", &content_type); 581 582 charset = _charSetForTextDecoding; 583 if (charset == B_MAIL_NULL_CONVERSION && content_type.HasString("charset")) { 584 const char* charset_string = content_type.FindString("charset"); 585 if (strcasecmp(charset_string, "us-ascii") == 0) { 586 charset = B_MAIL_US_ASCII_CONVERSION; 587 } else if (strcasecmp(charset_string, "utf-8") == 0) { 588 charset = B_MAIL_UTF8_CONVERSION; 589 } else { 590 const BCharacterSet* cs = BCharacterSetRoster::FindCharacterSetByName(charset_string); 591 if (cs != NULL) { 592 charset = cs->GetConversionID(); 593 } 594 } 595 } 596 597 encoding = encoding_for_cte(HeaderField("Content-Transfer-Encoding")); 598 599 char* buffer = (char*)malloc(raw_length + 1); 600 if (buffer == NULL) 601 return B_NO_MEMORY; 602 603 int32 bytes; 604 if ((bytes = raw_data->Read(buffer, raw_length)) < 0) 605 return B_IO_ERROR; 606 607 char* string = decoded.LockBuffer(bytes + 1); 608 bytes = decode(encoding, string, buffer, bytes, 0); 609 free(buffer); 610 buffer = NULL; 611 612 // Change line ends from \r\n to just \n. Though this won't work properly 613 // for UTF-16 because \r takes up two bytes rather than one. 614 char* dest; 615 char* src; 616 char* end = string + bytes; 617 for (dest = src = string; src < end; src++) { 618 if (*src != '\r') 619 *dest++ = *src; 620 } 621 decoded.UnlockBuffer(dest - string); 622 bytes = decoded.Length(); // Might have shrunk a bit. 623 624 // If the character set wasn't specified, try to guess. ISO-2022-JP 625 // contains the escape sequences ESC $ B or ESC $ @ to turn on 2 byte 626 // Japanese, and ESC ( J to switch to Roman, or sometimes ESC ( B for 627 // ASCII. We'll just try looking for the two switch to Japanese sequences. 628 629 if (charset == B_MAIL_NULL_CONVERSION) { 630 if (decoded.FindFirst ("\e$B") >= 0 || decoded.FindFirst ("\e$@") >= 0) 631 charset = B_JIS_CONVERSION; 632 else // Just assume the usual Latin-9 character set. 633 charset = B_ISO15_CONVERSION; 634 } 635 636 int32 state = 0; 637 int32 destLength = bytes * 3 /* in case it grows */ + 1 /* +1 so it isn't zero which crashes */; 638 string = text.LockBuffer(destLength); 639 mail_convert_to_utf8(charset, decoded.String(), &bytes, string, 640 &destLength, &state); 641 if (destLength > 0) 642 text.UnlockBuffer(destLength); 643 else { 644 text.UnlockBuffer(0); 645 text.SetTo(decoded); 646 } 647 648 raw_data = NULL; 649 return B_OK; 650 } 651 652 653 status_t 654 BTextMailComponent::RenderToRFC822(BPositionIO* render_to) 655 { 656 status_t status = ParseRaw(); 657 if (status < B_OK) 658 return status; 659 660 BMimeType type; 661 MIMEType(&type); 662 BString content_type; 663 content_type << type.Type(); // Preserve MIME type (e.g. text/html 664 665 for (uint32 i = 0; mail_charsets[i].charset != NULL; i++) { 666 if (mail_charsets[i].flavor == charset) { 667 content_type << "; charset=\"" << mail_charsets[i].charset << "\""; 668 break; 669 } 670 } 671 672 SetHeaderField("Content-Type", content_type.String()); 673 674 const char* transfer_encoding = NULL; 675 switch (encoding) { 676 case base64: 677 transfer_encoding = "base64"; 678 break; 679 case quoted_printable: 680 transfer_encoding = "quoted-printable"; 681 break; 682 case eight_bit: 683 transfer_encoding = "8bit"; 684 break; 685 case seven_bit: 686 default: 687 transfer_encoding = "7bit"; 688 break; 689 } 690 691 SetHeaderField("Content-Transfer-Encoding", transfer_encoding); 692 693 BMailComponent::RenderToRFC822(render_to); 694 695 BString modified = this->text; 696 BString alt; 697 698 int32 len = this->text.Length(); 699 if (len > 0) { 700 int32 dest_len = len * 5; 701 // Shift-JIS can have a 3 byte escape sequence and a 2 byte code for 702 // each character (which could just be 2 bytes in UTF-8, or even 1 byte 703 // if it's regular ASCII), so it can get quite a bit larger than the 704 // original text. Multiplying by 5 should make more than enough space. 705 char* raw = alt.LockBuffer(dest_len); 706 int32 state = 0; 707 mail_convert_from_utf8(charset, this->text.String(), &len, raw, 708 &dest_len, &state); 709 alt.UnlockBuffer(dest_len); 710 711 raw = modified.LockBuffer((alt.Length() * 3) + 1); 712 switch (encoding) { 713 case base64: 714 len = encode_base64(raw, alt.String(), alt.Length(), false); 715 raw[len] = 0; 716 break; 717 case quoted_printable: 718 len = encode_qp(raw, alt.String(), alt.Length(), false); 719 raw[len] = 0; 720 break; 721 case eight_bit: 722 case seven_bit: 723 default: 724 len = alt.Length(); 725 strcpy(raw, alt.String()); 726 } 727 modified.UnlockBuffer(len); 728 729 if (encoding != base64) // encode_base64 already does CRLF line endings. 730 modified.ReplaceAll("\n","\r\n"); 731 732 // There seem to be a possibility of NULL bytes in the text, so lets 733 // filter them out, shouldn't be any after the encoding stage. 734 735 char* string = modified.LockBuffer(modified.Length()); 736 for (int32 i = modified.Length(); i-- > 0;) { 737 if (string[i] != '\0') 738 continue; 739 740 puts("BTextMailComponent::RenderToRFC822: NULL byte in text!!"); 741 string[i] = ' '; 742 } 743 modified.UnlockBuffer(); 744 745 // word wrapping is already done by BeMail (user-configurable) 746 // and it does it *MUCH* nicer. 747 748 // //------Desperate bid to wrap lines 749 // int32 curr_line_length = 0; 750 // int32 last_space = 0; 751 // 752 // for (int32 i = 0; i < modified.Length(); i++) { 753 // if (isspace(modified.ByteAt(i))) 754 // last_space = i; 755 // 756 // if ((modified.ByteAt(i) == '\r') && (modified.ByteAt(i+1) == '\n')) 757 // curr_line_length = 0; 758 // else 759 // curr_line_length++; 760 // 761 // if (curr_line_length > 80) { 762 // if (last_space >= 0) { 763 // modified.Insert("\r\n",last_space); 764 // last_space = -1; 765 // curr_line_length = 0; 766 // } 767 // } 768 // } 769 } 770 modified << "\r\n"; 771 772 render_to->Write(modified.String(), modified.Length()); 773 774 return B_OK; 775 } 776 777 778 void BTextMailComponent::_ReservedText1() {} 779 void BTextMailComponent::_ReservedText2() {} 780