1 2 3 T U T O R I A L F O R M I M E + + 4 51. Introduction 6 7Welcome to MIME++, a C++ class library for creating, parsing, and modifying 8messages in MIME format. MIME++ has been designed specifically with the 9following objectives in mind: 10 11 * Create classes that directly correspond to the elements described in 12 RFC-822, RFC-2045, and other MIME-related documents. 13 14 * Create a library that is easy to use. 15 16 * Create a library that is extensible. 17 18MIME++ classes directly model the elements of the BNF grammar specified in 19RFC-822, RFC-2045, and RFC-2046. For this reason, I recommend that you 20understand these RFCs and keep a copy of them handy as you learn MIME++. 21If you know C++ well, and if you are familiar with the RFCs, you should find 22MIME++ easy to learn and use. If you are new to C++ and object-oriented 23programming, you will find in MIME++ some very good object-oriented 24techinques, and hopefully you will learn a lot. 25 26Before looking at the MIME++ classes, it is important to understand how 27MIME++ represents a message. There are two representations of a message. 28The first is a string representation, in which a message is considered 29simply a sequence of characters. The second is a 'broken-down' -- that is, 30parsed -- representation, in which the message is represented as a tree of 31components. 32 33The tree will be explained later, but for now, let's consider the 34relationship between the string representation and the broken-down 35representation. When you create a new message, the string representation 36is initially empty. After you set the contents of the broken-down 37representation, such as the header fields and the message body, you then 38assemble the message from its broken-down representation into its string 39representation. The assembling is done through a call to the Assemble() 40member function of the DwMessage class. Conversely, when you receive a 41message, it is received in its string representation, and you parse the 42message to create its broken-down representation. The parsing is done 43through a call to the Parse() member function of the DwMessage class. 44From the broken-down representation, you can access the header fields, the 45body, and so on. If you want to modify a received message, you can change 46the contents of the broken-down representation, then assemble the message 47to create the modified string representation. Because of the way MIME++ 48implements the broken-down representation, only those specific components 49that were modified in the broken-down representation will be modified in 50the new string representation. 51 52The broken-down representation takes the form of a tree. The idea for the 53tree comes from the idea that a message can be broken down into various 54components, and that the components form a hierarchy. At the highest 55level, we have the complete message. We can break the message down into a 56header and a body to arrive at the second-highest level. We can break the 57header down into a collection of header fields. We can break each header 58field down into a field-name and a field-body. If the header field is a 59structured field, we can further break down its field-body into components 60specific to that field-body, such as a local-part and domain for a 61mailbox. Now, we can think of each component of the message as a node in 62the tree. The top, or root, node is the message itself. Below that, the 63message node contains child nodes for the header and body; the header node 64contains a child node for each header field; and so on. Each node 65contains a substring of the entire message, and a node's string is the 66concatenation of all of its child nodes' strings. 67 68In the MIME++ implementation, the abstract base class DwMessageComponent 69encapsulates all the common attributes and behavior of the tree's nodes. 70The most important member functions of DwMessageComponent are Parse() and 71Assemble(), which are declared as pure virtual functions. Normally, you 72would use these member functions only as operations on objects of the 73class DwMessage, a subclass of DwMessageComponent. Parse() builds the 74entire tree of components with the DwMessage object at the root. 75Assemble() builds the string representation of the DwMessage object by 76traversing the tree and concatenating the strings of the leaf nodes. 77While every node in the tree is a DwMessageComponent, and therefore has a 78Parse() and Assemble() member function, you do not have to call these 79member functions for every node in the tree. The reason is that both of 80these functions traverse the subtree rooted at the current node. Parse() 81acts first on the current node, then calls the Parse() member function of 82its child nodes. Assemble() first calls the Assemble() member functions 83of a node's child nodes, then concatenates the string representations of 84its child nodes. Therefore, when you call Parse() or Assemble() for an 85object of the class DwMessage, Parse() or Assemble() will be called 86automatically for every component (that is, child node) in the message. 87 88DwMessageComponent also has one important attribute that you should be aware 89of. That attribute is an is-modified flag (aka dirty flag), which is 90cleared whenever Parse() or Assemble() is called, and is set whenever the 91broken-down representation is modified. To understand how this works, 92suppose you have just called Parse() on a DwMessage object to create its 93broken-down representation. If you add a new DwField object (representing a 94new header field) to the DwHeaders object (representing the header), the 95is-modified flag will be set for the DwHeaders object, indicating that the 96string representation of the DwHeaders object will have to be re-assembled 97from the header fields that it contains. When a node's is-modified flag is 98set, it also notifies its parent node to set its is-modified flag. Thus, 99when the DwHeaders object's is-modified flag is set, the DwMessage object 100that is its parent will also have its is-modified flag set. That way, when 101Assemble() is called for the DwMessage object, it will call the Assemble() 102member function for the DwHeaders object, as required. Notice that the value 103of having an is-modified flag is that it can purge the tree traversal when 104the string representation of a message is being assembled. 105 106One of the first classes you should become familiar with is the DwString 107class, which handles character strings in MIME++. DwString has been 108designed to handle very large character strings, so it may be different 109from string classes in other libraries. Most of the standard C library 110string fuctions have DwString counterparts in MIME++. These functions 111all start with "Dw", and include DwStrcpy(), DwStrcmp(), DwStrcasecmp(), 112and so on. In addition, the equality operators and assignment operators 113work as expected. If you have used string classes from other libraries, 114you will find DwString fairly intuitive. 115 116The following sections describe how to create, parse, and modify a 117message. You should also look at the example programs included with the 118distribution. These example programs are well-commented and use wrapper 119classes. The wrapper classes BasicMessage, MultipartMessage, and 120MessageWithAttachments, are designed with three purposes in mind. First, 121if your requirements are very modest -- say you just want to send a few 122files as attachments -- then you may find these classes to be adequate for 123your needs, and you will not have to learn the MIME++ library classes. 124Second, wrapper classes are the recommended way to use MIME++. You should 125consider starting with these classes and customizing them for your own 126application. Using wrapper classes will simplify the use of the MIME++ 127library, but will also help to shield your application from future changes 128in the MIME++ library. Third, these classes provide excellent examples for 129how to use the MIME++ library classes. 130 131The rest of this tutorial focuses on the library classes themselves. 132 133 1342. Creating a Message 135 136Creating a message with MIME++ involves instantiating a DwMessage object, 137setting values for its parts, and assembling the message into its final 138string representation. The following simple example shows how to 139accomplish this. 140 141 142 void SendMessage( 143 const char* aTo, 144 const char* aFrom, 145 const char* aSubject, 146 const char* aBody) 147 { 148 // Create an empty message 149 150 DwMessage msg; 151 152 // Set the header fields. 153 // [ Note that a temporary DwString object is created for 154 // the argument for FromString() using the 155 // DwString::DwString(const char*) constructor. ] 156 157 DwHeaders& headers = msg.Headers(); 158 headers.MessageId().CreateDefault(); 159 headers.Date().FromCalendarTime(time(NULL)); //current date, time 160 headers.To().FromString(aTo); 161 headers.From().FromString(aFrom); 162 headers.Subject().FromString(aSubject); 163 164 // Set the message body 165 166 msg.Body().FromString(aBody); 167 168 // Assemble the message from its parts 169 170 msg.Assemble(); 171 172 // Finally, send it. In this example, just print it to the 173 // cout stream. 174 175 cout << msg.AsString(); 176 } 177 178 179In this example, we set the fields 'Message-Id', 'Date', 'To', 'From', and 180'Subject', which are all documented in RFC-822. The MIME++ class DwHeaders 181directly supports all header fields documented in RFC-822, RFC-2045, and 182RFC-1036. To access the field-body for any one these fields, use the 183member function from DwHeaders that has a name corresponding to the 184field-name for that field. The correspondence between a field-name and 185the name of the member function in DwHeaders is consistent: hyphens are 186dropped and the first character after the hyphen is capitalized. Thus, 187field-name Content-type in RFC-1521 corresponds to the member function 188name ContentType. These field-body access functions create an empty field 189in the headers if that field does not already exist. To check if a 190particular field exists already, DwHeaders provides member functions 191HasXxxxx(); for example, HasSender(), HasMimeVersion(), or HasXref() 192will indicate whether the DwHeaders object has a 'Sender' field, a 193'MIME-Version' field, or an 'Xref' field, respectively. 194 195In the example, we used the FromString() member function of 196DwMessageComponent to set the string representation of the field-bodies. 197This is the simplest way to set the contents of a DwFieldBody object. 198Many of the field-bodies also have a broken-down represenation, and it is 199possible to set the parts of the broken-down representation. Consider, for 200example, the DwDateTime class, which represents the date-time element of the 201BNF grammar specified in RFC-822. In the example above, we did not set the 202string representation -- that would be more difficult and error prone. 203Instead we set the contents from the time_t value returned from a call to 204the ANSI C function time(). The DwDateTime class also contains member 205functions for setting individual attributes. For example, we could have 206used the following code: 207 208 DwDateTime& date = msg.Headers().Date(); 209 time_t t = time(NULL); 210 struct tm stm = *localtime(&t); 211 date.SetYear(stm.tm_year); 212 date.SetMonth(stm.tm_mon); 213 date.SetDay(stm.tm_mday); 214 date.SetHour(stm.tm_hour); 215 date.SetMinute(stm.tm_min); 216 217 2183. Parsing a Message 219 220Parsing a received message with MIME++ involves instantiating a DwMessage 221object, setting its string representation to contain the message, and then 222calling the Parse() member function of the DwMessage object. The 223following simple example shows how to accomplish this. 224 225 void ParseMessage(DwString& aMessageStr) 226 { 227 // Create a message object 228 // We can set the message's string representation directly from the 229 // constructor, as in the uncommented version. Or, we can use the 230 // default constructor and set its string representation using 231 // the member function DwMessage::FromString(), as in the 232 // commented version. 233 234 DwMessage msg(aMessageStr); 235 236 // Alternate technique: 237 // DwMessage msg; // Default constructor 238 // msg.FromString(aMessageStr); // Set its string representation 239 240 // Execute the parse method, which will create the broken-down 241 // representation (the tree representation, if you recall) 242 243 msg.Parse(); 244 245 // Print some of the header fields, just to show how it's done 246 247 // Date field. First check if the field exists, since 248 // DwHeaders::Date() will create it if is not found. 249 250 if (msg.Headers().HasDate()) { 251 cout << "Date of message is " 252 << msg.Headers().Date().AsString() 253 << '\n'; 254 } 255 256 // From field. Here we access the broken-down field body, too, 257 // to get the full name (which may be empty), the local part, 258 // and the domain of the first mailbox. (The 'From' field can 259 // have a list of mailboxes). 260 261 if (msg.Headers().HasFrom()) { 262 DwMailboxList& from = msg.Headers().From(); 263 cout << "Message is from "; 264 265 // Get first mailbox, then iterate through the list 266 267 int isFirst = 1; 268 DwMailbox* mb = from.FirstMailbox(); 269 while (mb) { 270 if (isFirst) { 271 isFirst = 0; 272 } 273 else { 274 cout << ", "; 275 } 276 DwString& fullName = mb->FullName(); 277 if (fullName != "") { 278 cout << fullName << '\n'; 279 } 280 else { 281 // Apparently, there is no full name, so use the email 282 // address 283 cout << mb->LocalPart() << '@' << mb->Domain() << '\n'; 284 } 285 mb = mb->Next(); 286 } 287 288 } 289 290 // Finally, print the message body, just to show how the body is 291 // retrieved. 292 293 cout << msg.Body().AsString() << '\n'; 294 } 295 296Once you have parsed the message, you can access any of its parts. The 297field-bodies of well-known header fields can be accessed by calling member 298functions of DwHeaders. Some examples follow. 299 300 DwMediaType& contType = msg.Headers().ContentType(); 301 DwMechanism& cte = msg.Headers().ContentTransferEncoding(); 302 DwDateTime& date = msg.Headers().Date(); 303 304The various subclasses of DwFieldBody, including DwMediaType, DwMechanism, 305and DwDateTime above, have member functions that allow you to access the parts 306of the field-body. For example, DwMediaType has member functions to allow 307you to access its type, subtype, and parameters. If the message is a 308multipart message, you may access the body parts by calling member 309functions of the class DwBody. See the example code in multipar.cpp for 310an example of how to do this. 311 312 3134. Modifying a Message 314 315Modifying a message combines the procedures of parsing a message and 316creating a message. First, parse the message, as explained above. Then 317set the values of the components -- field-bodies, new fields, new body 318parts, or what have you -- that you wish to modify. Finally, call the 319Assemble() member function of the DwMessage object to reassemble the 320message. You can then access the modified message by calling 321DwMessage::AsString(). These final steps are the same as those involved 322in creating a new message. 323 324 3255. Customizing MIME++ Classes 326 327MIME++ has been designed to be easily customizable. Typically, you 328customize C++ library classes through inheritance. MIME++ allows you to 329create subclasses of most of its library classes in order to change their 330behavior. MIME++ also includes certain 'hooks', which make it far easier 331to customize certain parts of the library. 332 333The most common customization is that of changing the way header fields 334are dealt with. This could include adding the ability to handle certain 335non-standard header fields, or to change the way the field-bodies of 336certain standard header fields are interpreted or parsed. As an example of 337the former customization, you may want to add the 'X-status' field or 338'X-sender' field to your messages. As an example of the latter, you may 339want to change DwMediaType so that it will handle other MIME subtypes. 340 341Let's begin with the latter situation -- that of subclassing DwMediaType. 342Obviously, you will have to become familiar with DwMediaType and its 343superclasses before you change its behavior. Then, at a minimum, you will 344want to provide your own implementation of the virtual member functions 345Parse() and Assemble(). Once you feel comfortable with the behavior of 346the behavior of your new class -- call it MyMediaType -- you will have to 347take the right steps to ensure that the MIME++ library internal routines 348will create objects of type MyMediaType, and not DwMediaType. There are 349three such steps. 350 351First, define a function NewMyMediaType(), matching the prototype 352 353 DwMediaType* NewMyMediaType( 354 const DwString& aStr, 355 DwMessage* aParent) 356 357that creates a new instance of MyMediaType and returns it. Set the static 358data member DwMediaType::sNewMediaType to point to this function. 359DwMediaType::sNewMediaType is normally NULL, meaning that no user-defined 360function is available. When you set this static data member, however, 361MIME++'s internal routines will call your own function, and will therefore 362be able to create instances of your subclass. 363 364Second, make sure you have reimplemented the virtual function 365DwMediaType::Clone() to return a clone of your own subclassed object. 366Clone() serves as a 'virtual constructor'. (See the discussion of virtual 367constructors in Stroustrup's _The C++ Programming Language_, 2nd Ed). 368 369Third, you should define a function CreateFieldBody(), matching the 370prototype 371 372 DwFieldBody* CreateFieldBody( 373 const DwString& aFieldName, 374 const DwString& aFieldBody, 375 DwMessageComponent* aParent) 376 377that returns an object of a subclass of DwFieldBody. (DwFieldBody is a 378superclass of MyMediaType). CreateFieldBody() is similar to the 379NewMyMediaType() function already described, except that its first 380argument supplies the field-name for the particular field currently being 381handled by MIME++. CreateFieldBody() should examine the field-name, 382create an object of the appropriate subclass of DwFieldBody, and return a 383pointer to the object. In this particular case, you need to make sure 384that when the field-name is 'Content-Type' you return an object of the 385class MyMediaType. Set the hook for CreateFieldBody() setting the static 386data member DwField::sCreateFieldBody to point to your CreateFieldBody() 387function. DwField::sCreateFieldBody is normally NULL when no user 388function is provided. 389 390These three steps are sufficient to ensure that your subclass of 391DwMediaType is integrated with the other MIME++ classes. 392 393The other customization task mentioned above is that of adding support for 394a non-standard header field. There is a simple way to do this, and a way 395that involves creating a subclass of DwHeaders. You can access any header 396field by calling DwHeaders's member functions. In fact, you can iterate 397over all the header fields if you would like. Therefore, the really 398simple way is just to not change anything and just use existing member 399functions. The relevant functions include DwHeaders::HasField(), which will 400return a boolean value indicating if the header has the specified field, 401and DwHeaders::FieldBody(), which will return the DwFieldBody object 402associated with a specified field. [ Note that DwHeaders::FieldBody() will 403create a field if it is not found. ] The default DwFieldBody subclass, 404which applies to all header fields not recognized by MIME++, is DwText, 405which is suitable for the unstructured field-bodies described in RFC-822 406such as 'Subject', 'Comments', and so on. If a DwText object is suitable 407for your non-standard header field, then you don't have to do anything at all. 408Suppose, however, that you want an object of your own subclass of 409DwFieldBody, say StatusFieldBody, to be attached to the 'X-status' field. 410In this case, you will need to set the hook DwField::sCreateFieldBody as 411discussed above. Your CreateFieldBody() function should return an 412instance of StatusFieldBody whenever the field-name is 'X-status'. 413 414Finally, while you can access any header field using DwHeaders's member 415functions, you may want to create your own subclass of DwHeaders for some 416reason or other -- maybe to add a convenience function to access the 417'X-status' header field. To ensure that your new class is integrated with 418the library routines, you basically follow steps 1 and 2 above for 419subclassing DwFieldBody. First, define a function NewMyHeaders() and set the 420static data member DwHeaders::sNewHeaders to point to your function. Second, 421make sure you have reimplemented the virtual function DwHeaders::Clone() to 422return an instance of your subclass. Step 3 for subclassing DwFieldBody 423does not apply when subclassing DwHeaders. 424 425 4266. Getting Help 427 428I will try to help anyone who needs help specific to MIME++. I won't try 429to answer general questions about C++ that could be answered by any C++ 430expert. Bug reports will receive the highest priority. Other questions 431about how to do something I will try to answer in time, but I ask for your 432patience. If you have any comments -- perhaps maybe you know of a better 433way to do something -- please send them. My preferred email is 434dwsauder@fwb.gulf.net, but dwsauder@tasc.com is also acceptable. 435 436Good luck! 437 438$Revision: 1.3 $ 439$Date: 1997/09/27 11:53:27 $ 440