2 * Copyright 2007-2015, Haiku Inc. All Rights Reserved.
3 * Copyright 2001-2004 Dr. Zoidberg Enterprises. All rights reserved.
5 * Distributed under the terms of the MIT License.
9 //! The main general purpose mail message class
12 #include <MailMessage.h>
18 #include <sys/utsname.h>
20 #include <parsedate.h>
22 #include <Directory.h>
26 #include <FindDirectory.h>
28 #include <MailAttachment.h>
29 #include <MailDaemon.h>
30 #include <MailSettings.h>
31 #include <Messenger.h>
36 #include <StringList.h>
38 #include <MailPrivate.h>
39 #include <mail_util.h>
42 using namespace BPrivate
;
45 //-------Change the following!----------------------
46 #define mime_boundary "----------Zoidberg-BeMail-temp--------"
47 #define mime_warning "This is a multipart message in MIME format."
50 BEmailMessage::BEmailMessage(BPositionIO
* file
, bool own
, uint32 defaultCharSet
)
52 BMailContainer(defaultCharSet
),
60 BMailSettings settings
;
61 fAccountID
= settings
.DefaultOutboundAccount();
67 SetToRFC822(file
, ~0L);
71 BEmailMessage::BEmailMessage(const entry_ref
* ref
, uint32 defaultCharSet
)
73 BMailContainer(defaultCharSet
),
79 BMailSettings settings
;
80 fAccountID
= settings
.DefaultOutboundAccount();
83 fStatus
= static_cast<BFile
*>(fData
)->SetTo(ref
, B_READ_ONLY
);
86 SetToRFC822(fData
, ~0L);
90 BEmailMessage::~BEmailMessage()
100 BEmailMessage::InitCheck() const
107 BEmailMessage::ReplyMessage(mail_reply_to_mode replyTo
, bool accountFromMail
,
108 const char* quoteStyle
)
110 BEmailMessage
* reply
= new BEmailMessage
;
114 if (replyTo
== B_MAIL_REPLY_TO_ALL
) {
115 reply
->SetTo(From());
118 get_address_list(list
, CC(), extract_address
);
119 get_address_list(list
, To(), extract_address
);
121 // Filter out the sender
122 BMailAccounts accounts
;
123 BMailAccountSettings
* account
= accounts
.AccountByID(Account());
126 sender
= account
->ReturnAddress();
127 extract_address(sender
);
131 for (int32 i
= list
.CountItems(); i
-- > 0;) {
132 char* address
= (char*)list
.RemoveItem((int32
)0);
134 // Add everything which is not the sender and not already in the
136 if (sender
.ICompare(address
) && cc
.FindFirst(address
) < 0) {
147 reply
->SetCC(cc
.String());
148 } else if (replyTo
== B_MAIL_REPLY_TO_SENDER
|| ReplyTo() == NULL
)
149 reply
->SetTo(From());
151 reply
->SetTo(ReplyTo());
153 // Set special "In-Reply-To:" header (used for threading)
154 const char* messageID
= fBody
? fBody
->HeaderField("Message-Id") : NULL
;
155 if (messageID
!= NULL
)
156 reply
->SetHeaderField("In-Reply-To", messageID
);
159 reply
->SetBodyTextTo(BodyText());
161 reply
->Body()->Quote(quoteStyle
);
163 // Set the subject (and add a "Re:" if needed)
164 BString string
= Subject();
165 if (string
.ICompare("re:", 3) != 0)
166 string
.Prepend("Re: ");
167 reply
->SetSubject(string
.String());
169 // set the matching outbound chain
171 reply
->SendViaAccountFrom(this);
178 BEmailMessage::ForwardMessage(bool accountFromMail
, bool includeAttachments
)
180 BString header
= "------ Forwarded Message: ------\n";
181 header
<< "To: " << To() << '\n';
182 header
<< "From: " << From() << '\n';
184 // Can use CC rather than "Cc" since display only.
185 header
<< "CC: " << CC() << '\n';
187 header
<< "Subject: " << Subject() << '\n';
188 header
<< "Date: " << Date() << "\n\n";
189 if (fTextBody
!= NULL
)
190 header
<< fTextBody
->Text() << '\n';
191 BEmailMessage
*message
= new BEmailMessage();
192 message
->SetBodyTextTo(header
.String());
195 BString subject
= Subject();
196 if (subject
.IFindFirst("fwd") == B_ERROR
197 && subject
.IFindFirst("forward") == B_ERROR
198 && subject
.FindFirst("FW") == B_ERROR
)
200 message
->SetSubject(subject
.String());
202 if (includeAttachments
) {
203 for (int32 i
= 0; i
< CountComponents(); i
++) {
204 BMailComponent
* component
= GetComponent(i
);
205 if (component
== fTextBody
|| component
== NULL
)
208 //---I am ashamed to have the written the code between here and the next comment
209 // ... and you still managed to get it wrong ;-)), axeld.
210 // we should really move this stuff into copy constructors
211 // or something like that
214 component
->RenderToRFC822(&io
);
215 BMailComponent
* clone
= component
->WhatIsThis();
216 io
.Seek(0, SEEK_SET
);
217 clone
->SetToRFC822(&io
, io
.BufferLength(), true);
218 message
->AddComponent(clone
);
222 message
->SendViaAccountFrom(this);
229 BEmailMessage::To() const
231 return HeaderField("To");
236 BEmailMessage::From() const
238 return HeaderField("From");
243 BEmailMessage::ReplyTo() const
245 return HeaderField("Reply-To");
250 BEmailMessage::CC() const
252 return HeaderField("Cc");
253 // Note case of CC is "Cc" in our internal headers.
258 BEmailMessage::Subject() const
260 return HeaderField("Subject");
265 BEmailMessage::Date() const
267 const char* dateField
= HeaderField("Date");
268 if (dateField
== NULL
)
271 return ParseDateWithTimeZone(dateField
);
276 BEmailMessage::Priority() const
279 const char* priorityString
;
281 /* The usual values are a number from 1 to 5, or one of three words:
282 X-Priority: 1 and/or X-MSMail-Priority: High
283 X-Priority: 3 and/or X-MSMail-Priority: Normal
284 X-Priority: 5 and/or X-MSMail-Priority: Low
285 Also plain Priority: is "normal", "urgent" or "non-urgent", see RFC 1327. */
287 priorityString
= HeaderField("Priority");
288 if (priorityString
== NULL
)
289 priorityString
= HeaderField("X-Priority");
290 if (priorityString
== NULL
)
291 priorityString
= HeaderField("X-Msmail-Priority");
292 if (priorityString
== NULL
)
294 priorityNumber
= atoi (priorityString
);
295 if (priorityNumber
!= 0) {
296 if (priorityNumber
> 5)
298 if (priorityNumber
< 1)
300 return priorityNumber
;
302 if (strcasecmp (priorityString
, "Low") == 0
303 || strcasecmp (priorityString
, "non-urgent") == 0)
305 if (strcasecmp (priorityString
, "High") == 0
306 || strcasecmp (priorityString
, "urgent") == 0)
313 BEmailMessage::SetSubject(const char* subject
, uint32 charset
,
314 mail_encoding encoding
)
316 SetHeaderField("Subject", subject
, charset
, encoding
);
321 BEmailMessage::SetReplyTo(const char* replyTo
, uint32 charset
,
322 mail_encoding encoding
)
324 SetHeaderField("Reply-To", replyTo
, charset
, encoding
);
329 BEmailMessage::SetFrom(const char* from
, uint32 charset
, mail_encoding encoding
)
331 SetHeaderField("From", from
, charset
, encoding
);
336 BEmailMessage::SetTo(const char* to
, uint32 charset
, mail_encoding encoding
)
338 SetHeaderField("To", to
, charset
, encoding
);
343 BEmailMessage::SetCC(const char* cc
, uint32 charset
, mail_encoding encoding
)
345 // For consistency with our header names, use Cc as the name.
346 SetHeaderField("Cc", cc
, charset
, encoding
);
351 BEmailMessage::SetBCC(const char* bcc
)
359 BEmailMessage::SetPriority(int to
)
367 sprintf (tempString
, "%d", to
);
368 SetHeaderField("X-Priority", tempString
);
370 SetHeaderField("Priority", "urgent");
371 SetHeaderField("X-Msmail-Priority", "High");
372 } else if (to
>= 4) {
373 SetHeaderField("Priority", "non-urgent");
374 SetHeaderField("X-Msmail-Priority", "Low");
376 SetHeaderField("Priority", "normal");
377 SetHeaderField("X-Msmail-Priority", "Normal");
383 BEmailMessage::GetName(char* name
, int32 maxLength
) const
385 if (name
== NULL
|| maxLength
<= 0)
388 if (BFile
* file
= dynamic_cast<BFile
*>(fData
)) {
389 status_t status
= file
->ReadAttr(B_MAIL_ATTR_NAME
, B_STRING_TYPE
, 0,
391 name
[maxLength
- 1] = '\0';
393 return status
>= 0 ? B_OK
: status
;
395 // TODO: look at From header? But usually there is
396 // a file since only the BeMail GUI calls this.
402 BEmailMessage::GetName(BString
* name
) const
404 char* buffer
= name
->LockBuffer(B_FILE_NAME_LENGTH
);
405 status_t status
= GetName(buffer
, B_FILE_NAME_LENGTH
);
406 name
->UnlockBuffer();
413 BEmailMessage::SendViaAccountFrom(BEmailMessage
* message
)
416 if (message
->GetAccountName(name
) < B_OK
) {
417 // just return the message with the default account
421 SendViaAccount(name
);
426 BEmailMessage::SendViaAccount(const char* accountName
)
428 BMailAccounts accounts
;
429 BMailAccountSettings
* account
= accounts
.AccountByName(accountName
);
431 SendViaAccount(account
->AccountID());
436 BEmailMessage::SendViaAccount(int32 account
)
438 fAccountID
= account
;
440 BMailAccounts accounts
;
441 BMailAccountSettings
* accountSettings
= accounts
.AccountByID(fAccountID
);
444 if (accountSettings
) {
445 from
<< '\"' << accountSettings
->RealName() << "\" <"
446 << accountSettings
->ReturnAddress() << '>';
453 BEmailMessage::Account() const
460 BEmailMessage::GetAccountName(BString
& accountName
) const
462 BFile
* file
= dynamic_cast<BFile
*>(fData
);
467 size_t read
= file
->ReadAttr(B_MAIL_ATTR_ACCOUNT
, B_INT32_TYPE
, 0,
468 &accountID
, sizeof(int32
));
469 if (read
< sizeof(int32
))
472 BMailAccounts accounts
;
473 BMailAccountSettings
* account
= accounts
.AccountByID(accountID
);
475 accountName
= account
->Name();
484 BEmailMessage::AddComponent(BMailComponent
* component
)
486 status_t status
= B_OK
;
488 if (fComponentCount
== 0)
490 else if (fComponentCount
== 1) {
491 BMIMEMultipartMailContainer
*container
492 = new BMIMEMultipartMailContainer(
493 mime_boundary
, mime_warning
, _charSetForTextDecoding
);
494 status
= container
->AddComponent(fBody
);
496 status
= container
->AddComponent(component
);
499 BMIMEMultipartMailContainer
* container
500 = dynamic_cast<BMIMEMultipartMailContainer
*>(fBody
);
501 if (container
== NULL
)
502 return B_MISMATCHED_VALUES
;
504 status
= container
->AddComponent(component
);
514 BEmailMessage::RemoveComponent(BMailComponent
* /*component*/)
516 // not yet implemented
517 // BeMail/Enclosures.cpp:169: contains a warning about this fact
523 BEmailMessage::RemoveComponent(int32
/*index*/)
525 // not yet implemented
531 BEmailMessage::GetComponent(int32 i
, bool parseNow
)
533 if (BMIMEMultipartMailContainer
* container
534 = dynamic_cast<BMIMEMultipartMailContainer
*>(fBody
))
535 return container
->GetComponent(i
, parseNow
);
537 if (i
< fComponentCount
)
545 BEmailMessage::CountComponents() const
547 return fComponentCount
;
552 BEmailMessage::Attach(entry_ref
* ref
, bool includeAttributes
)
554 if (includeAttributes
)
555 AddComponent(new BAttributedMailAttachment(ref
));
557 AddComponent(new BSimpleMailAttachment(ref
));
562 BEmailMessage::IsComponentAttachment(int32 i
)
564 if ((i
>= fComponentCount
) || (fComponentCount
== 0))
567 if (fComponentCount
== 1)
568 return fBody
->IsAttachment();
570 BMIMEMultipartMailContainer
* container
571 = dynamic_cast<BMIMEMultipartMailContainer
*>(fBody
);
572 if (container
== NULL
)
575 BMailComponent
* component
= container
->GetComponent(i
);
576 if (component
== NULL
)
579 return component
->IsAttachment();
584 BEmailMessage::SetBodyTextTo(const char* text
)
586 if (fTextBody
== NULL
) {
587 fTextBody
= new BTextMailComponent
;
588 AddComponent(fTextBody
);
591 fTextBody
->SetText(text
);
596 BEmailMessage::Body()
598 if (fTextBody
== NULL
)
599 fTextBody
= _RetrieveTextBody(fBody
);
606 BEmailMessage::BodyText()
611 return fTextBody
->Text();
616 BEmailMessage::SetBody(BTextMailComponent
* body
)
618 if (fTextBody
!= NULL
) {
620 // removing doesn't exist for now
621 // RemoveComponent(fTextBody);
625 AddComponent(fTextBody
);
632 BEmailMessage::_RetrieveTextBody(BMailComponent
* component
)
634 BTextMailComponent
* body
= dynamic_cast<BTextMailComponent
*>(component
);
638 BMIMEMultipartMailContainer
* container
639 = dynamic_cast<BMIMEMultipartMailContainer
*>(component
);
640 if (container
!= NULL
) {
641 for (int32 i
= 0; i
< container
->CountComponents(); i
++) {
642 if ((component
= container
->GetComponent(i
)) == NULL
)
645 switch (component
->ComponentType()) {
646 case B_MAIL_PLAIN_TEXT_BODY
:
647 // AttributedAttachment returns the MIME type of its
648 // contents, so we have to use dynamic_cast here
649 body
= dynamic_cast<BTextMailComponent
*>(
650 container
->GetComponent(i
));
655 case B_MAIL_MULTIPART_CONTAINER
:
656 body
= _RetrieveTextBody(container
->GetComponent(i
));
668 BEmailMessage::SetToRFC822(BPositionIO
* mailFile
, size_t length
,
671 if (BFile
* file
= dynamic_cast<BFile
*>(mailFile
)) {
672 file
->ReadAttr(B_MAIL_ATTR_ACCOUNT_ID
, B_INT32_TYPE
, 0, &fAccountID
,
676 mailFile
->Seek(0, SEEK_END
);
677 length
= mailFile
->Position();
678 mailFile
->Seek(0, SEEK_SET
);
680 fStatus
= BMailComponent::SetToRFC822(mailFile
, length
, parseNow
);
684 fBody
= WhatIsThis();
686 mailFile
->Seek(0, SEEK_SET
);
687 fStatus
= fBody
->SetToRFC822(mailFile
, length
, parseNow
);
691 // Move headers that we use to us, everything else to fBody
693 for (int32 i
= 0; (name
= fBody
->HeaderAt(i
)) != NULL
; i
++) {
694 if (strcasecmp(name
, "Subject") != 0
695 && strcasecmp(name
, "To") != 0
696 && strcasecmp(name
, "From") != 0
697 && strcasecmp(name
, "Reply-To") != 0
698 && strcasecmp(name
, "Cc") != 0
699 && strcasecmp(name
, "Priority") != 0
700 && strcasecmp(name
, "X-Priority") != 0
701 && strcasecmp(name
, "X-Msmail-Priority") != 0
702 && strcasecmp(name
, "Date") != 0) {
707 fBody
->RemoveHeader("Subject");
708 fBody
->RemoveHeader("To");
709 fBody
->RemoveHeader("From");
710 fBody
->RemoveHeader("Reply-To");
711 fBody
->RemoveHeader("Cc");
712 fBody
->RemoveHeader("Priority");
713 fBody
->RemoveHeader("X-Priority");
714 fBody
->RemoveHeader("X-Msmail-Priority");
715 fBody
->RemoveHeader("Date");
718 if (BMIMEMultipartMailContainer
* container
719 = dynamic_cast<BMIMEMultipartMailContainer
*>(fBody
))
720 fComponentCount
= container
->CountComponents();
727 BEmailMessage::RenderToRFC822(BPositionIO
* file
)
730 return B_MAIL_INVALID_MAIL
;
734 if (From() == NULL
) {
735 // set the "From:" string
736 SendViaAccount(fAccountID
);
740 get_address_list(recipientList
, To(), extract_address
);
741 get_address_list(recipientList
, CC(), extract_address
);
742 get_address_list(recipientList
, fBCC
, extract_address
);
745 for (int32 i
= recipientList
.CountItems(); i
-- > 0;) {
746 char *address
= (char *)recipientList
.RemoveItem((int32
)0);
748 recipients
<< '<' << address
<< '>';
755 // add the date field
756 int32 creationTime
= time(NULL
);
760 localtime_r(&creationTime
, &tm
);
762 size_t length
= strftime(date
, sizeof(date
),
763 "%a, %d %b %Y %H:%M:%S", &tm
);
765 // GMT offsets are full hours, yes, but you never know :-)
766 snprintf(date
+ length
, sizeof(date
) - length
, " %+03d%02d",
767 tm
.tm_gmtoff
/ 3600, (tm
.tm_gmtoff
/ 60) % 60);
769 SetHeaderField("Date", date
);
774 // empirical evidence indicates message id must be enclosed in
775 // angle brackets and there must be an "at" symbol in it
778 messageID
<< system_time();
779 messageID
<< "-BeMail@";
782 if (gethostname(host
, sizeof(host
)) < 0 || !host
[0])
783 strcpy(host
, "zoidberg");
788 SetHeaderField("Message-Id", messageID
.String());
790 status_t err
= BMailComponent::RenderToRFC822(file
);
794 file
->Seek(-2, SEEK_CUR
);
795 // Remove division between headers
797 err
= fBody
->RenderToRFC822(file
);
801 // Set the message file's attributes. Do this after the rest of the file
802 // is filled in, in case the daemon attempts to send it before it is ready
803 // (since the daemon may send it when it sees the status attribute getting
804 // set to "Pending").
806 if (BFile
* attributed
= dynamic_cast <BFile
*>(file
)) {
807 BNodeInfo(attributed
).SetType(B_MAIL_TYPE
);
809 attributed
->WriteAttrString(B_MAIL_ATTR_RECIPIENTS
,&recipients
);
814 attributed
->WriteAttrString(B_MAIL_ATTR_TO
, &attr
);
816 attributed
->WriteAttrString(B_MAIL_ATTR_CC
, &attr
);
818 attributed
->WriteAttrString(B_MAIL_ATTR_SUBJECT
, &attr
);
820 attributed
->WriteAttrString(B_MAIL_ATTR_REPLY
, &attr
);
822 attributed
->WriteAttrString(B_MAIL_ATTR_FROM
, &attr
);
823 if (Priority() != 3 /* Normal is 3 */) {
824 sprintf(attr
.LockBuffer(40), "%d", Priority());
825 attr
.UnlockBuffer(-1);
826 attributed
->WriteAttrString(B_MAIL_ATTR_PRIORITY
, &attr
);
829 attributed
->WriteAttrString(B_MAIL_ATTR_STATUS
, &attr
);
831 attributed
->WriteAttrString(B_MAIL_ATTR_MIME
, &attr
);
833 attributed
->WriteAttr(B_MAIL_ATTR_ACCOUNT
, B_INT32_TYPE
, 0,
834 &fAccountID
, sizeof(int32
));
836 attributed
->WriteAttr(B_MAIL_ATTR_WHEN
, B_TIME_TYPE
, 0, &creationTime
,
838 int32 flags
= B_MAIL_PENDING
| B_MAIL_SAVE
;
839 attributed
->WriteAttr(B_MAIL_ATTR_FLAGS
, B_INT32_TYPE
, 0, &flags
,
842 attributed
->WriteAttr(B_MAIL_ATTR_ACCOUNT_ID
, B_INT32_TYPE
, 0,
843 &fAccountID
, sizeof(int32
));
851 BEmailMessage::RenderTo(BDirectory
* dir
, BEntry
* msg
)
854 char numericDateString
[40];
855 struct tm timeFields
;
858 // Generate a file name for the outgoing message. See also
859 // FolderFilter::ProcessMailMessage which does something similar for
860 // incoming messages.
862 BString name
= Subject();
863 SubjectToThread(name
);
864 // Extract the core subject words.
865 if (name
.Length() <= 0)
867 if (name
[0] == '.') {
868 // Avoid hidden files, starting with a dot.
872 // Convert the date into a year-month-day fixed digit width format, so that
873 // sorting by file name will give all the messages with the same subject in
876 localtime_r (¤tTime
, &timeFields
);
877 sprintf (numericDateString
, "%04d%02d%02d%02d%02d%02d",
878 timeFields
.tm_year
+ 1900, timeFields
.tm_mon
+ 1, timeFields
.tm_mday
,
879 timeFields
.tm_hour
, timeFields
.tm_min
, timeFields
.tm_sec
);
880 name
<< " " << numericDateString
;
883 extract_address_name(worker
);
884 name
<< " " << worker
;
886 name
.Truncate(222); // reserve space for the uniquer
888 // Get rid of annoying characters which are hard to use in the shell.
889 name
.ReplaceAll('/','_');
890 name
.ReplaceAll('\'','_');
891 name
.ReplaceAll('"','_');
892 name
.ReplaceAll('!','_');
893 name
.ReplaceAll('<','_');
894 name
.ReplaceAll('>','_');
896 // Remove multiple spaces.
897 while (name
.FindFirst(" ") >= 0)
898 name
.Replace(" ", " ", 1024);
900 int32 uniquer
= time(NULL
);
905 while ((exists
= dir
->Contains(worker
.String())) && --tries
> 0) {
907 uniquer
+= (rand() >> 16) - 16384;
910 worker
<< ' ' << uniquer
;
914 printf("could not create mail! (should be: %s)\n", worker
.String());
917 status_t status
= dir
->CreateFile(worker
.String(), &file
);
922 msg
->SetTo(dir
,worker
.String());
924 return RenderToRFC822(&file
);
929 BEmailMessage::Send(bool sendNow
)
931 BMailAccounts accounts
;
932 BMailAccountSettings
* account
= accounts
.AccountByID(fAccountID
);
933 if (account
== NULL
|| !account
->HasOutbound()) {
934 account
= accounts
.AccountByID(
935 BMailSettings().DefaultOutboundAccount());
938 SendViaAccount(account
->AccountID());
942 if (account
->OutboundSettings().FindString("path", &path
) != B_OK
) {
943 BPath defaultMailOutPath
;
944 if (find_directory(B_USER_DIRECTORY
, &defaultMailOutPath
) != B_OK
945 || defaultMailOutPath
.Append("mail/out") != B_OK
)
946 path
= "/boot/home/mail/out";
948 path
= defaultMailOutPath
.Path();
951 create_directory(path
.String(), 0777);
952 BDirectory
directory(path
.String());
956 status_t status
= RenderTo(&directory
, &message
);
957 if (status
>= B_OK
&& sendNow
) {
958 // TODO: check whether or not the internet connection is available
959 BMessenger
daemon(B_MAIL_DAEMON_SIGNATURE
);
960 if (!daemon
.IsValid())
961 return B_MAIL_NO_DAEMON
;
963 BMessage
msg(kMsgSendMessages
);
964 msg
.AddInt32("account", fAccountID
);
966 message
.GetPath(&path
);
967 msg
.AddString("message_path", path
.Path());
968 daemon
.SendMessage(&msg
);
975 void BEmailMessage::_ReservedMessage1() {}
976 void BEmailMessage::_ReservedMessage2() {}
977 void BEmailMessage::_ReservedMessage3() {}