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